diff --git a/.eslintignore b/.eslintignore index e67eceb534da..c327eb4e0146 100644 --- a/.eslintignore +++ b/.eslintignore @@ -125,14 +125,6 @@ devtools/server/tests/browser/** !devtools/server/tests/browser/browser_webextension_inspected_window.js devtools/server/tests/mochitest/** devtools/server/tests/unit/** -devtools/shared/*.js -!devtools/shared/async-storage.js -!devtools/shared/async-utils.js -!devtools/shared/defer.js -!devtools/shared/event-emitter.js -!devtools/shared/indentation.js -!devtools/shared/loader-plugin-raw.jsm -!devtools/shared/task.js devtools/shared/apps/** devtools/shared/client/** devtools/shared/discovery/** diff --git a/browser/config/mozconfigs/macosx64/beta b/browser/config/mozconfigs/macosx64/beta new file mode 100644 index 000000000000..64cf69d866f7 --- /dev/null +++ b/browser/config/mozconfigs/macosx64/beta @@ -0,0 +1,15 @@ +MOZ_AUTOMATION_SDK=${MOZ_AUTOMATION_SDK-1} + +if [ -n "$ENABLE_RELEASE_PROMOTION" ]; then + MOZ_AUTOMATION_UPLOAD_SYMBOLS=1 + MOZ_AUTOMATION_UPDATE_PACKAGING=1 +fi + +. "$topsrcdir/browser/config/mozconfigs/macosx64/common-opt" + +ac_add_options --enable-official-branding +ac_add_options --enable-verify-mar + +. "$topsrcdir/build/mozconfig.rust" +. "$topsrcdir/build/mozconfig.common.override" +. "$topsrcdir/build/mozconfig.cache" diff --git a/browser/config/mozconfigs/macosx64/common-opt b/browser/config/mozconfigs/macosx64/common-opt new file mode 100644 index 000000000000..413fb6dcca6e --- /dev/null +++ b/browser/config/mozconfigs/macosx64/common-opt @@ -0,0 +1,15 @@ +# This file is sourced by the nightly, beta, and release mozconfigs. + +. $topsrcdir/build/macosx/mozconfig.common + +ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL} +ac_add_options --with-google-api-keyfile=/builds/gapi.data +ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key + +# Needed to enable breakpad in application.ini +export MOZILLA_OFFICIAL=1 + +export MOZ_TELEMETRY_REPORTING=1 + +# Package js shell. +export MOZ_PACKAGE_JSSHELL=1 diff --git a/browser/config/mozconfigs/macosx64/nightly b/browser/config/mozconfigs/macosx64/nightly index 12fec0474334..0a915da54660 100644 --- a/browser/config/mozconfigs/macosx64/nightly +++ b/browser/config/mozconfigs/macosx64/nightly @@ -1,20 +1,19 @@ -. $topsrcdir/build/macosx/mozconfig.common +. "$topsrcdir/browser/config/mozconfigs/macosx64/common-opt" +ac_add_options --disable-install-strip ac_add_options --enable-verify-mar +ac_add_options --enable-profiling +ac_add_options --enable-instruments -# Needed to enable breakpad in application.ini -export MOZILLA_OFFICIAL=1 - -# Enable Telemetry -export MOZ_TELEMETRY_REPORTING=1 +# Cross-compiled builds fail when dtrace is enabled +if test `uname -s` != Linux; then + ac_add_options --enable-dtrace +fi if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then ac_add_options --with-macbundlename-prefix=Firefox fi -# Package js shell. -export MOZ_PACKAGE_JSSHELL=1 - ac_add_options --with-branding=browser/branding/nightly . "$topsrcdir/build/mozconfig.rust" diff --git a/browser/config/mozconfigs/macosx64/release b/browser/config/mozconfigs/macosx64/release new file mode 100644 index 000000000000..562a16b50ebe --- /dev/null +++ b/browser/config/mozconfigs/macosx64/release @@ -0,0 +1,21 @@ +# This make file should be identical to the beta mozconfig, apart from the +# safeguard below +MOZ_AUTOMATION_SDK=${MOZ_AUTOMATION_SDK-1} + +if [ -n "$ENABLE_RELEASE_PROMOTION" ]; then + MOZ_AUTOMATION_UPLOAD_SYMBOLS=1 + MOZ_AUTOMATION_UPDATE_PACKAGING=1 +fi + +. "$topsrcdir/browser/config/mozconfigs/macosx64/common-opt" + +ac_add_options --enable-official-branding +ac_add_options --enable-verify-mar + +# safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in +# defines.sh during the beta cycle +export BUILDING_RELEASE=1 + +. "$topsrcdir/build/mozconfig.rust" +. "$topsrcdir/build/mozconfig.common.override" +. "$topsrcdir/build/mozconfig.cache" diff --git a/browser/config/mozconfigs/whitelist b/browser/config/mozconfigs/whitelist index 445629ef57ef..b18c0d3e145d 100644 --- a/browser/config/mozconfigs/whitelist +++ b/browser/config/mozconfigs/whitelist @@ -5,7 +5,7 @@ whitelist = { 'nightly': {}, } -all_platforms = ['win64', 'win32', 'linux32', 'linux64', 'macosx-universal'] +all_platforms = ['win64', 'win32', 'linux32', 'linux64', 'macosx64'] for platform in all_platforms: whitelist['nightly'][platform] = [ @@ -15,7 +15,7 @@ for platform in all_platforms: 'mk_add_options CLIENT_PY_ARGS="--hg-options=\'--verbose --time\' --hgtool=../tools/buildfarm/utils/hgtool.py --skip-chatzilla --skip-comm --skip-inspector --tinderbox-print"' ] -for platform in ['linux32', 'linux64', 'macosx-universal']: +for platform in ['linux32', 'linux64', 'macosx64']: whitelist['nightly'][platform] += [ 'mk_add_options MOZ_MAKE_FLAGS="-j4"', ] @@ -42,7 +42,7 @@ whitelist['nightly']['linux64'] += [ '. "$topsrcdir/build/mozconfig.cache"', ] -whitelist['nightly']['macosx-universal'] += [ +whitelist['nightly']['macosx64'] += [ 'if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then', 'ac_add_options --with-macbundlename-prefix=Firefox', 'fi', diff --git a/build/clang-plugin/clang-plugin.cpp b/build/clang-plugin/clang-plugin.cpp index 7ca86c4c4247..595253d21409 100644 --- a/build/clang-plugin/clang-plugin.cpp +++ b/build/clang-plugin/clang-plugin.cpp @@ -851,7 +851,8 @@ AST_MATCHER(BinaryOperator, isInSystemHeader) { AST_MATCHER(BinaryOperator, isInWhitelistForNaNExpr) { const char* whitelist[] = { "SkScalar.h", - "json_writer.cpp" + "json_writer.cpp", + "State.cpp" }; SourceLocation Loc = Node.getOperatorLoc(); diff --git a/build/compare-mozconfig/compare-mozconfigs-wrapper.py b/build/compare-mozconfig/compare-mozconfigs-wrapper.py index 1c281a93f9a6..f737afe15086 100644 --- a/build/compare-mozconfig/compare-mozconfigs-wrapper.py +++ b/build/compare-mozconfig/compare-mozconfigs-wrapper.py @@ -19,8 +19,7 @@ log = logging.getLogger(__name__) def determine_platform(): platform_mapping = {'WINNT': {'x86_64': 'win64', 'i686': 'win32'}, - 'Darwin': {'x86_64': 'macosx-universal', - 'i386':'macosx-universal'}, + 'Darwin': {'x86_64': 'macosx64'}, 'Linux': {'x86_64': 'linux64', 'i686': 'linux32'}} diff --git a/build/macosx/cross-mozconfig.common b/build/macosx/cross-mozconfig.common index 8e56394d00bf..fc58da15d80b 100644 --- a/build/macosx/cross-mozconfig.common +++ b/build/macosx/cross-mozconfig.common @@ -15,6 +15,12 @@ fi mk_add_options "export LD_LIBRARY_PATH=$topsrcdir/clang/lib" CROSS_CCTOOLS_PATH=$topsrcdir/cctools +# This SDK was copied from a local XCode install and uploaded to tooltool. +# Generate the tarball by running this command with the proper SDK version: +# sdk_path=$(xcrun --sdk macosx10.12 --show-sdk-path) +# tar -C $(dirname ${sdk_path}) -cHjf /tmp/$(basename ${sdk_path}).tar.bz2 $(basename ${sdk_path}) +# Upload the resulting tarball from /tmp to tooltool, and change the entry in +# `browser/config/tooltool-manifests/macosx64/cross-releng.manifest`. CROSS_SYSROOT=$topsrcdir/MacOSX10.7.sdk CROSS_PRIVATE_FRAMEWORKS=$CROSS_SYSROOT/System/Library/PrivateFrameworks FLAGS="-target x86_64-apple-darwin10 -mlinker-version=136 -B $CROSS_CCTOOLS_PATH/bin -isysroot $CROSS_SYSROOT" diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn index bf39c549e552..6fb6ce096f79 100644 --- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -14,8 +14,7 @@ devtools.jar: content/projecteditor/chrome/content/projecteditor-test.xul (projecteditor/chrome/content/projecteditor-test.xul) content/projecteditor/chrome/content/projecteditor-loader.js (projecteditor/chrome/content/projecteditor-loader.js) content/netmonitor/netmonitor.xul (netmonitor/netmonitor.xul) - content/netmonitor/netmonitor-controller.js (netmonitor/netmonitor-controller.js) - content/netmonitor/netmonitor-view.js (netmonitor/netmonitor-view.js) + content/netmonitor/netmonitor.js (netmonitor/netmonitor.js) content/webconsole/webconsole.xul (webconsole/webconsole.xul) * content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul) content/scratchpad/scratchpad.js (scratchpad/scratchpad.js) diff --git a/devtools/client/netmonitor/actions/batching.js b/devtools/client/netmonitor/actions/batching.js new file mode 100644 index 000000000000..fd68db185a32 --- /dev/null +++ b/devtools/client/netmonitor/actions/batching.js @@ -0,0 +1,42 @@ +/* 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 { + BATCH_ACTIONS, + BATCH_ENABLE, + BATCH_RESET, +} = require("../constants"); + +/** + * Process multiple actions at once as part of one dispatch, and produce only one + * state update at the end. This action is not processed by any reducer, but by a + * special store enhancer. + */ +function batchActions(actions) { + return { + type: BATCH_ACTIONS, + actions + }; +} + +function batchEnable(enabled) { + return { + type: BATCH_ENABLE, + enabled + }; +} + +function batchReset() { + return { + type: BATCH_RESET, + }; +} + +module.exports = { + batchActions, + batchEnable, + batchReset, +}; diff --git a/devtools/client/netmonitor/actions/filters.js b/devtools/client/netmonitor/actions/filters.js index 0082c64dfdd9..b8d8726afd39 100644 --- a/devtools/client/netmonitor/actions/filters.js +++ b/devtools/client/netmonitor/actions/filters.js @@ -42,12 +42,12 @@ function enableFilterTypeOnly(filter) { /** * Set filter text. * - * @param {string} url - A filter text is going to be set + * @param {string} text - A filter text is going to be set */ -function setFilterText(url) { +function setFilterText(text) { return { type: SET_FILTER_TEXT, - url, + text, }; } diff --git a/devtools/client/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js index 29ce28fd5d50..110f73ce76d8 100644 --- a/devtools/client/netmonitor/actions/index.js +++ b/devtools/client/netmonitor/actions/index.js @@ -4,8 +4,20 @@ "use strict"; +const batching = require("./batching"); const filters = require("./filters"); const requests = require("./requests"); +const selection = require("./selection"); +const sort = require("./sort"); +const timingMarkers = require("./timing-markers"); const ui = require("./ui"); -module.exports = Object.assign({}, filters, requests, ui); +Object.assign(exports, + batching, + filters, + requests, + selection, + sort, + timingMarkers, + ui +); diff --git a/devtools/client/netmonitor/actions/moz.build b/devtools/client/netmonitor/actions/moz.build index 9082b8bd1b88..e81d2714edfa 100644 --- a/devtools/client/netmonitor/actions/moz.build +++ b/devtools/client/netmonitor/actions/moz.build @@ -3,8 +3,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( + 'batching.js', 'filters.js', 'index.js', 'requests.js', + 'selection.js', + 'sort.js', + 'timing-markers.js', 'ui.js', ) diff --git a/devtools/client/netmonitor/actions/requests.js b/devtools/client/netmonitor/actions/requests.js index ae794a437496..77871a0ff293 100644 --- a/devtools/client/netmonitor/actions/requests.js +++ b/devtools/client/netmonitor/actions/requests.js @@ -5,21 +5,61 @@ "use strict"; const { - UPDATE_REQUESTS, + ADD_REQUEST, + UPDATE_REQUEST, + CLONE_SELECTED_REQUEST, + REMOVE_SELECTED_CUSTOM_REQUEST, + CLEAR_REQUESTS, } = require("../constants"); -/** - * Update request items - * - * @param {array} requests - visible request items - */ -function updateRequests(items) { +function addRequest(id, data, batch) { return { - type: UPDATE_REQUESTS, - items, + type: ADD_REQUEST, + id, + data, + meta: { batch }, + }; +} + +function updateRequest(id, data, batch) { + return { + type: UPDATE_REQUEST, + id, + data, + meta: { batch }, + }; +} + +/** + * Clone the currently selected request, set the "isCustom" attribute. + * Used by the "Edit and Resend" feature. + */ +function cloneSelectedRequest() { + return { + type: CLONE_SELECTED_REQUEST + }; +} + +/** + * Remove a request from the list. Supports removing only cloned requests with a + * "isCustom" attribute. Other requests never need to be removed. + */ +function removeSelectedCustomRequest() { + return { + type: REMOVE_SELECTED_CUSTOM_REQUEST + }; +} + +function clearRequests() { + return { + type: CLEAR_REQUESTS }; } module.exports = { - updateRequests, + addRequest, + updateRequest, + cloneSelectedRequest, + removeSelectedCustomRequest, + clearRequests, }; diff --git a/devtools/client/netmonitor/actions/selection.js b/devtools/client/netmonitor/actions/selection.js new file mode 100644 index 000000000000..dffc6d8cc000 --- /dev/null +++ b/devtools/client/netmonitor/actions/selection.js @@ -0,0 +1,67 @@ +/* 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 { getDisplayedRequests } = require("../selectors/index"); +const { SELECT_REQUEST, PRESELECT_REQUEST } = require("../constants"); + +/** + * When a new request with a given id is added in future, select it immediately. + * Used by the "Edit and Resend" feature, where we know in advance the ID of the + * request, at a time when it wasn't sent yet. + */ +function preselectRequest(id) { + return { + type: PRESELECT_REQUEST, + id + }; +} + +/** + * Select request with a given id. + */ +function selectRequest(id) { + return { + type: SELECT_REQUEST, + id + }; +} + +const PAGE_SIZE_ITEM_COUNT_RATIO = 5; + +/** + * Move the selection up to down according to the "delta" parameter. Possible values: + * - Number: positive or negative, move up or down by specified distance + * - "PAGE_UP" | "PAGE_DOWN" (String): page up or page down + * - +Infinity | -Infinity: move to the start or end of the list + */ +function selectDelta(delta) { + return (dispatch, getState) => { + const state = getState(); + const requests = getDisplayedRequests(state); + + if (requests.isEmpty()) { + return; + } + + const selIndex = requests.findIndex(r => r.id === state.requests.selectedId); + + if (delta === "PAGE_DOWN") { + delta = Math.ceil(requests.size / PAGE_SIZE_ITEM_COUNT_RATIO); + } else if (delta === "PAGE_UP") { + delta = -Math.ceil(requests.size / PAGE_SIZE_ITEM_COUNT_RATIO); + } + + const newIndex = Math.min(Math.max(0, selIndex + delta), requests.size - 1); + const newItem = requests.get(newIndex); + dispatch(selectRequest(newItem.id)); + }; +} + +module.exports = { + preselectRequest, + selectRequest, + selectDelta, +}; diff --git a/devtools/client/netmonitor/actions/sort.js b/devtools/client/netmonitor/actions/sort.js new file mode 100644 index 000000000000..2dd02373ec50 --- /dev/null +++ b/devtools/client/netmonitor/actions/sort.js @@ -0,0 +1,18 @@ +/* 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 { SORT_BY } = require("../constants"); + +function sortBy(sortType) { + return { + type: SORT_BY, + sortType + }; +} + +module.exports = { + sortBy +}; diff --git a/devtools/client/netmonitor/actions/timing-markers.js b/devtools/client/netmonitor/actions/timing-markers.js new file mode 100644 index 000000000000..4f1363a70637 --- /dev/null +++ b/devtools/client/netmonitor/actions/timing-markers.js @@ -0,0 +1,19 @@ +/* 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 { ADD_TIMING_MARKER, CLEAR_TIMING_MARKERS } = require("../constants"); + +exports.addTimingMarker = (marker) => { + return { + type: ADD_TIMING_MARKER, + marker + }; +}; + +exports.clearTimingMarkers = () => { + return { + type: CLEAR_TIMING_MARKERS + }; +}; diff --git a/devtools/client/netmonitor/actions/ui.js b/devtools/client/netmonitor/actions/ui.js index 554921bc2781..31539518fb72 100644 --- a/devtools/client/netmonitor/actions/ui.js +++ b/devtools/client/netmonitor/actions/ui.js @@ -6,7 +6,7 @@ const { OPEN_SIDEBAR, - TOGGLE_SIDEBAR, + WATERFALL_RESIZE, } = require("../constants"); /** @@ -25,12 +25,21 @@ function openSidebar(open) { * Toggle sidebar open state. */ function toggleSidebar() { + return (dispatch, getState) => dispatch(openSidebar(!getState().ui.sidebarOpen)); +} + +/** + * Waterfall width has changed (likely on window resize). Update the UI. + */ +function resizeWaterfall(width) { return { - type: TOGGLE_SIDEBAR, + type: WATERFALL_RESIZE, + width }; } module.exports = { openSidebar, toggleSidebar, + resizeWaterfall, }; diff --git a/devtools/client/netmonitor/components/clear-button.js b/devtools/client/netmonitor/components/clear-button.js index cccff81fb9dd..5ef1a73211fd 100644 --- a/devtools/client/netmonitor/components/clear-button.js +++ b/devtools/client/netmonitor/components/clear-button.js @@ -2,12 +2,12 @@ * 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/. */ -/* globals NetMonitorView */ - "use strict"; const { DOM } = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); const { L10N } = require("../l10n"); +const Actions = require("../actions/index"); const { button } = DOM; @@ -15,15 +15,18 @@ const { button } = DOM; * Clear button component * A type of tool button is responsible for cleaning network requests. */ -function ClearButton() { +function ClearButton({ onClick }) { return button({ id: "requests-menu-clear-button", className: "devtools-button devtools-clear-icon", title: L10N.getStr("netmonitor.toolbar.clear"), - onClick: () => { - NetMonitorView.RequestsMenu.clear(); - }, + onClick, }); } -module.exports = ClearButton; +module.exports = connect( + undefined, + dispatch => ({ + onClick: () => dispatch(Actions.clearRequests()) + }) +)(ClearButton); diff --git a/devtools/client/netmonitor/components/moz.build b/devtools/client/netmonitor/components/moz.build index 02655b710f23..c4977531fdb0 100644 --- a/devtools/client/netmonitor/components/moz.build +++ b/devtools/client/netmonitor/components/moz.build @@ -2,9 +2,19 @@ # 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/. +DIRS += [ + 'shared', +] + DevToolsModules( 'clear-button.js', 'filter-buttons.js', + 'request-list-content.js', + 'request-list-empty.js', + 'request-list-header.js', + 'request-list-item.js', + 'request-list-tooltip.js', + 'request-list.js', 'search-box.js', 'summary-button.js', 'toggle-button.js', diff --git a/devtools/client/netmonitor/components/request-list-content.js b/devtools/client/netmonitor/components/request-list-content.js new file mode 100644 index 000000000000..c5f029600d2b --- /dev/null +++ b/devtools/client/netmonitor/components/request-list-content.js @@ -0,0 +1,255 @@ +/* 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/. */ +/* globals NetMonitorView */ + +"use strict"; + +const { Task } = require("devtools/shared/task"); +const { createClass, createFactory, DOM } = require("devtools/client/shared/vendor/react"); +const { div } = DOM; +const Actions = require("../actions/index"); +const RequestListItem = createFactory(require("./request-list-item")); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { setTooltipImageContent, + setTooltipStackTraceContent } = require("./request-list-tooltip"); +const { getDisplayedRequests, + getWaterfallScale } = require("../selectors/index"); +const { KeyCodes } = require("devtools/client/shared/keycodes"); + +// tooltip show/hide delay in ms +const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500; + +/** + * Renders the actual contents of the request list. + */ +const RequestListContent = createClass({ + displayName: "RequestListContent", + + componentDidMount() { + // Set the CSS variables for waterfall scaling + this.setScalingStyles(); + + // Install event handler for displaying a tooltip + this.props.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, { + toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY, + interactive: true + }); + + // Install event handler to hide the tooltip on scroll + this.refs.contentEl.addEventListener("scroll", this.onScroll, true); + }, + + componentWillUpdate() { + // Check if the list is scrolled to bottom, before UI update + this.shouldScrollBottom = this.isScrolledToBottom(); + }, + + componentDidUpdate(prevProps) { + // Update the CSS variables for waterfall scaling after props change + this.setScalingStyles(); + + // Keep the list scrolled to bottom if a new row was added + if (this.shouldScrollBottom) { + let node = this.refs.contentEl; + node.scrollTop = node.scrollHeight; + } + }, + + componentWillUnmount() { + this.refs.contentEl.removeEventListener("scroll", this.onScroll, true); + + // Uninstall the tooltip event handler + this.props.tooltip.stopTogglingOnHover(); + }, + + /** + * Set the CSS variables for waterfall scaling. If React supported setting CSS + * variables as part of the "style" property of a DOM element, we would use that. + * + * However, React doesn't support this, so we need to use a hack and update the + * DOM element directly: https://github.com/facebook/react/issues/6411 + */ + setScalingStyles(prevProps) { + const { scale } = this.props; + if (scale == this.currentScale) { + return; + } + + this.currentScale = scale; + + const { style } = this.refs.contentEl; + style.removeProperty("--timings-scale"); + style.removeProperty("--timings-rev-scale"); + style.setProperty("--timings-scale", scale); + style.setProperty("--timings-rev-scale", 1 / scale); + }, + + isScrolledToBottom() { + const { contentEl } = this.refs; + const lastChildEl = contentEl.lastElementChild; + + if (!lastChildEl) { + return false; + } + + let lastChildRect = lastChildEl.getBoundingClientRect(); + let contentRect = contentEl.getBoundingClientRect(); + + return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom; + }, + + /** + * The predicate used when deciding whether a popup should be shown + * over a request item or not. + * + * @param nsIDOMNode target + * The element node currently being hovered. + * @param object tooltip + * The current tooltip instance. + * @return {Promise} + */ + onHover: Task.async(function* (target, tooltip) { + let itemEl = target.closest(".request-list-item"); + if (!itemEl) { + return false; + } + let itemId = itemEl.dataset.id; + if (!itemId) { + return false; + } + let requestItem = this.props.displayedRequests.find(r => r.id == itemId); + if (!requestItem) { + return false; + } + + if (requestItem.responseContent && target.closest(".requests-menu-icon-and-file")) { + return setTooltipImageContent(tooltip, itemEl, requestItem); + } else if (requestItem.cause && target.closest(".requests-menu-cause-stack")) { + return setTooltipStackTraceContent(tooltip, requestItem); + } + + return false; + }), + + /** + * Scroll listener for the requests menu view. + */ + onScroll() { + this.props.tooltip.hide(); + }, + + /** + * Handler for keyboard events. For arrow up/down, page up/down, home/end, + * move the selection up or down. + */ + onKeyDown(e) { + let delta; + + switch (e.keyCode) { + case KeyCodes.DOM_VK_UP: + case KeyCodes.DOM_VK_LEFT: + delta = -1; + break; + case KeyCodes.DOM_VK_DOWN: + case KeyCodes.DOM_VK_RIGHT: + delta = +1; + break; + case KeyCodes.DOM_VK_PAGE_UP: + delta = "PAGE_UP"; + break; + case KeyCodes.DOM_VK_PAGE_DOWN: + delta = "PAGE_DOWN"; + break; + case KeyCodes.DOM_VK_HOME: + delta = -Infinity; + break; + case KeyCodes.DOM_VK_END: + delta = +Infinity; + break; + } + + if (delta) { + // Prevent scrolling when pressing navigation keys. + e.preventDefault(); + e.stopPropagation(); + this.props.onSelectDelta(delta); + } + }, + + /** + * If selection has just changed (by keyboard navigation), don't keep the list + * scrolled to bottom, but allow scrolling up with the selection. + */ + onFocusedNodeChange() { + this.shouldScrollBottom = false; + }, + + /** + * If a focused item was unmounted, transfer the focus to the container element. + */ + onFocusedNodeUnmount() { + if (this.refs.contentEl) { + this.refs.contentEl.focus(); + } + }, + + render() { + const { selectedRequestId, + displayedRequests, + firstRequestStartedMillis, + onItemMouseDown, + onItemContextMenu, + onSecurityIconClick } = this.props; + + return div( + { + ref: "contentEl", + className: "requests-menu-contents", + tabIndex: 0, + onKeyDown: this.onKeyDown, + }, + displayedRequests.map((item, index) => RequestListItem({ + key: item.id, + item, + index, + isSelected: item.id === selectedRequestId, + firstRequestStartedMillis, + onMouseDown: e => onItemMouseDown(e, item.id), + onContextMenu: e => onItemContextMenu(e, item.id), + onSecurityIconClick: e => onSecurityIconClick(e, item), + onFocusedNodeChange: this.onFocusedNodeChange, + onFocusedNodeUnmount: this.onFocusedNodeUnmount, + })) + ); + }, +}); + +module.exports = connect( + state => ({ + displayedRequests: getDisplayedRequests(state), + selectedRequestId: state.requests.selectedId, + scale: getWaterfallScale(state), + firstRequestStartedMillis: state.requests.firstStartedMillis, + tooltip: NetMonitorView.RequestsMenu.tooltip, + }), + dispatch => ({ + onItemMouseDown: (e, item) => dispatch(Actions.selectRequest(item)), + onItemContextMenu: (e, item) => { + e.preventDefault(); + NetMonitorView.RequestsMenu.contextMenu.open(e); + }, + onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)), + /** + * A handler that opens the security tab in the details view if secure or + * broken security indicator is clicked. + */ + onSecurityIconClick: (e, item) => { + const { securityState } = item; + if (securityState && securityState !== "insecure") { + // Choose the security tab. + NetMonitorView.NetworkDetails.widget.selectedIndex = 5; + } + }, + }) +)(RequestListContent); diff --git a/devtools/client/netmonitor/components/request-list-empty.js b/devtools/client/netmonitor/components/request-list-empty.js new file mode 100644 index 000000000000..f4fe56bc1c70 --- /dev/null +++ b/devtools/client/netmonitor/components/request-list-empty.js @@ -0,0 +1,65 @@ +/* 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/. */ +/* globals NetMonitorView */ + +"use strict"; + +const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react"); +const { L10N } = require("../l10n"); +const { div, span, button } = DOM; +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +/** + * UI displayed when the request list is empty. Contains instructions on reloading + * the page and on triggering performance analysis of the page. + */ +const RequestListEmptyNotice = createClass({ + displayName: "RequestListEmptyNotice", + + propTypes: { + onReloadClick: PropTypes.func.isRequired, + onPerfClick: PropTypes.func.isRequired, + }, + + render() { + return div( + { + id: "requests-menu-empty-notice", + className: "request-list-empty-notice", + }, + div({ id: "notice-reload-message" }, + span(null, L10N.getStr("netmonitor.reloadNotice1")), + button( + { + id: "requests-menu-reload-notice-button", + className: "devtools-toolbarbutton", + "data-standalone": true, + onClick: this.props.onReloadClick, + }, + L10N.getStr("netmonitor.reloadNotice2") + ), + span(null, L10N.getStr("netmonitor.reloadNotice3")) + ), + div({ id: "notice-perf-message" }, + span(null, L10N.getStr("netmonitor.perfNotice1")), + button({ + id: "requests-menu-perf-notice-button", + title: L10N.getStr("netmonitor.perfNotice3"), + className: "devtools-button", + "data-standalone": true, + onClick: this.props.onPerfClick, + }), + span(null, L10N.getStr("netmonitor.perfNotice2")) + ) + ); + } +}); + +module.exports = connect( + undefined, + dispatch => ({ + onPerfClick: e => NetMonitorView.toggleFrontendMode(), + onReloadClick: e => NetMonitorView.reloadPage(), + }) +)(RequestListEmptyNotice); diff --git a/devtools/client/netmonitor/components/request-list-header.js b/devtools/client/netmonitor/components/request-list-header.js new file mode 100644 index 000000000000..d28226eccf2b --- /dev/null +++ b/devtools/client/netmonitor/components/request-list-header.js @@ -0,0 +1,197 @@ +/* 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/. */ + +/* globals document */ + +"use strict"; + +const { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react"); +const { div, button } = DOM; +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { L10N } = require("../l10n"); +const { getWaterfallScale } = require("../selectors/index"); +const Actions = require("../actions/index"); +const WaterfallBackground = require("../waterfall-background"); + +// ms +const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; +// px +const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; + +const REQUEST_TIME_DECIMALS = 2; + +const HEADERS = [ + { name: "status", label: "status3" }, + { name: "method" }, + { name: "file", boxName: "icon-and-file" }, + { name: "domain", boxName: "security-and-domain" }, + { name: "cause" }, + { name: "type" }, + { name: "transferred" }, + { name: "size" }, + { name: "waterfall" } +]; + +/** + * Render the request list header with sorting arrows for columns. + * Displays tick marks in the waterfall column header. + * Also draws the waterfall background canvas and updates it when needed. + */ +const RequestListHeader = createClass({ + displayName: "RequestListHeader", + + propTypes: { + sort: PropTypes.object, + scale: PropTypes.number, + waterfallWidth: PropTypes.number, + onHeaderClick: PropTypes.func.isRequired, + }, + + componentDidMount() { + this.background = new WaterfallBackground(document); + this.background.draw(this.props); + }, + + componentDidUpdate() { + this.background.draw(this.props); + }, + + componentWillUnmount() { + this.background.destroy(); + this.background = null; + }, + + render() { + const { sort, scale, waterfallWidth, onHeaderClick } = this.props; + + return div( + { id: "requests-menu-toolbar", className: "devtools-toolbar" }, + div({ id: "toolbar-labels" }, + HEADERS.map(header => { + const name = header.name; + const boxName = header.boxName || name; + const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`); + + let sorted, sortedTitle; + const active = sort.type == name ? true : undefined; + if (active) { + sorted = sort.ascending ? "ascending" : "descending"; + sortedTitle = L10N.getStr(sort.ascending + ? "networkMenu.sortedAsc" + : "networkMenu.sortedDesc"); + } + + return div( + { + id: `requests-menu-${boxName}-header-box`, + key: name, + className: `requests-menu-header requests-menu-${boxName}`, + // Used to style the next column. + "data-active": active, + }, + button( + { + id: `requests-menu-${name}-button`, + className: `requests-menu-header-button requests-menu-${name}`, + "data-sorted": sorted, + title: sortedTitle, + onClick: () => onHeaderClick(name), + }, + name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label) + : div({ className: "button-text" }, label), + div({ className: "button-icon" }) + ) + ); + }) + ) + ); + } +}); + +/** + * Build the waterfall header - timing tick marks with the right spacing + */ +function waterfallDivisionLabels(waterfallWidth, scale) { + let labels = []; + + // Build new millisecond tick labels... + let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE; + let scaledStep = scale * timingStep; + + // Ignore any divisions that would end up being too close to each other. + while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) { + scaledStep *= 2; + } + + // Insert one label for each division on the current scale. + for (let x = 0; x < waterfallWidth; x += scaledStep) { + let millisecondTime = x / scale; + + let normalizedTime = millisecondTime; + let divisionScale = "millisecond"; + + // If the division is greater than 1 minute. + if (normalizedTime > 60000) { + normalizedTime /= 60000; + divisionScale = "minute"; + } else if (normalizedTime > 1000) { + // If the division is greater than 1 second. + normalizedTime /= 1000; + divisionScale = "second"; + } + + // Showing too many decimals is bad UX. + if (divisionScale == "millisecond") { + normalizedTime |= 0; + } else { + normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS); + } + + let width = (x + scaledStep | 0) - (x | 0); + // Adjust the first marker for the borders + if (x == 0) { + width -= 2; + } + // Last marker doesn't need a width specified at all + if (x + scaledStep >= waterfallWidth) { + width = undefined; + } + + labels.push(div( + { + key: labels.length, + className: "requests-menu-timings-division", + "data-division-scale": divisionScale, + style: { width } + }, + L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime) + )); + } + + return labels; +} + +function WaterfallLabel(waterfallWidth, scale, label) { + let className = "button-text requests-menu-waterfall-label-wrapper"; + + if (scale != null) { + label = waterfallDivisionLabels(waterfallWidth, scale); + className += " requests-menu-waterfall-visible"; + } + + return div({ className }, label); +} + +module.exports = connect( + state => ({ + sort: state.sort, + scale: getWaterfallScale(state), + waterfallWidth: state.ui.waterfallWidth, + firstRequestStartedMillis: state.requests.firstStartedMillis, + timingMarkers: state.timingMarkers, + }), + dispatch => ({ + onHeaderClick: type => dispatch(Actions.sortBy(type)), + }) +)(RequestListHeader); diff --git a/devtools/client/netmonitor/components/request-list-item.js b/devtools/client/netmonitor/components/request-list-item.js new file mode 100644 index 000000000000..a7976ea2ae29 --- /dev/null +++ b/devtools/client/netmonitor/components/request-list-item.js @@ -0,0 +1,346 @@ +/* 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 { createClass, PropTypes, DOM } = require("devtools/client/shared/vendor/react"); +const { div, span, img } = DOM; +const { L10N } = require("../l10n"); +const { getFormattedSize } = require("../utils/format-utils"); +const { getAbbreviatedMimeType } = require("../request-utils"); + +/** + * Render one row in the request list. + */ +const RequestListItem = createClass({ + displayName: "RequestListItem", + + propTypes: { + item: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + isSelected: PropTypes.bool.isRequired, + firstRequestStartedMillis: PropTypes.number.isRequired, + onContextMenu: PropTypes.func.isRequired, + onMouseDown: PropTypes.func.isRequired, + onSecurityIconClick: PropTypes.func.isRequired, + }, + + componentDidMount() { + if (this.props.isSelected) { + this.refs.el.focus(); + } + }, + + shouldComponentUpdate(nextProps) { + return !relevantPropsEqual(this.props.item, nextProps.item) + || this.props.index !== nextProps.index + || this.props.isSelected !== nextProps.isSelected + || this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis; + }, + + componentDidUpdate(prevProps) { + if (!prevProps.isSelected && this.props.isSelected) { + this.refs.el.focus(); + if (this.props.onFocusedNodeChange) { + this.props.onFocusedNodeChange(); + } + } + }, + + componentWillUnmount() { + // If this node is being destroyed and has focus, transfer the focus manually + // to the parent tree component. Otherwise, the focus will get lost and keyboard + // navigation in the tree will stop working. This is a workaround for a XUL bug. + // See bugs 1259228 and 1152441 for details. + // DE-XUL: Remove this hack once all usages are only in HTML documents. + if (this.props.isSelected) { + this.refs.el.blur(); + if (this.props.onFocusedNodeUnmount) { + this.props.onFocusedNodeUnmount(); + } + } + }, + + render() { + const { + item, + index, + isSelected, + firstRequestStartedMillis, + onContextMenu, + onMouseDown, + onSecurityIconClick + } = this.props; + + let classList = [ "request-list-item" ]; + if (isSelected) { + classList.push("selected"); + } + classList.push(index % 2 ? "odd" : "even"); + + return div( + { + ref: "el", + className: classList.join(" "), + "data-id": item.id, + tabIndex: 0, + onContextMenu, + onMouseDown, + }, + StatusColumn(item), + MethodColumn(item), + FileColumn(item), + DomainColumn(item, onSecurityIconClick), + CauseColumn(item), + TypeColumn(item), + TransferredSizeColumn(item), + ContentSizeColumn(item), + WaterfallColumn(item, firstRequestStartedMillis) + ); + } +}); + +/** + * Used by shouldComponentUpdate: compare two items, and compare only properties + * relevant for rendering the RequestListItem. Other properties (like request and + * response headers, cookies, bodies) are ignored. These are very useful for the + * sidebar details, but not here. + */ +const RELEVANT_ITEM_PROPS = [ + "status", + "statusText", + "fromCache", + "fromServiceWorker", + "method", + "url", + "responseContentDataUri", + "remoteAddress", + "securityState", + "cause", + "mimeType", + "contentSize", + "transferredSize", + "startedMillis", + "totalTime", + "eventTimings", +]; + +function relevantPropsEqual(item1, item2) { + return item1 === item2 || RELEVANT_ITEM_PROPS.every(p => item1[p] === item2[p]); +} + +function StatusColumn(item) { + const { status, statusText, fromCache, fromServiceWorker } = item; + + let code, title; + + if (status) { + if (fromCache) { + code = "cached"; + } else if (fromServiceWorker) { + code = "service worker"; + } else { + code = status; + } + + if (statusText) { + title = `${status} ${statusText}`; + if (fromCache) { + title += " (cached)"; + } + if (fromServiceWorker) { + title += " (service worker)"; + } + } + } + + return div({ className: "requests-menu-subitem requests-menu-status", title }, + div({ className: "requests-menu-status-icon", "data-code": code }), + span({ className: "subitem-label requests-menu-status-code" }, status) + ); +} + +function MethodColumn(item) { + const { method } = item; + return div({ className: "requests-menu-subitem requests-menu-method-box" }, + span({ className: "subitem-label requests-menu-method" }, method) + ); +} + +function FileColumn(item) { + const { urlDetails, responseContentDataUri } = item; + + return div({ className: "requests-menu-subitem requests-menu-icon-and-file" }, + img({ + className: "requests-menu-icon", + src: responseContentDataUri, + hidden: !responseContentDataUri, + "data-type": responseContentDataUri ? "thumbnail" : undefined + }), + div( + { + className: "subitem-label requests-menu-file", + title: urlDetails.unicodeUrl + }, + urlDetails.baseNameWithQuery + ) + ); +} + +function DomainColumn(item, onSecurityIconClick) { + const { urlDetails, remoteAddress, securityState } = item; + + let iconClassList = [ "requests-security-state-icon" ]; + let iconTitle; + if (urlDetails.isLocal) { + iconClassList.push("security-state-local"); + iconTitle = L10N.getStr("netmonitor.security.state.secure"); + } else if (securityState) { + iconClassList.push(`security-state-${securityState}`); + iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`); + } + + let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : ""); + + return div( + { className: "requests-menu-subitem requests-menu-security-and-domain" }, + div({ + className: iconClassList.join(" "), + title: iconTitle, + onClick: onSecurityIconClick, + }), + span({ className: "subitem-label requests-menu-domain", title }, urlDetails.host) + ); +} + +function CauseColumn(item) { + const { cause } = item; + + let causeType = ""; + let causeUri = undefined; + let causeHasStack = false; + + if (cause) { + causeType = cause.type; + causeUri = cause.loadingDocumentUri; + causeHasStack = cause.stacktrace && cause.stacktrace.length > 0; + } + + return div( + { className: "requests-menu-subitem requests-menu-cause", title: causeUri }, + span({ className: "requests-menu-cause-stack", hidden: !causeHasStack }, "JS"), + span({ className: "subitem-label" }, causeType) + ); +} + +const CONTENT_MIME_TYPE_ABBREVIATIONS = { + "ecmascript": "js", + "javascript": "js", + "x-javascript": "js" +}; + +function TypeColumn(item) { + const { mimeType } = item; + let abbrevType; + if (mimeType) { + abbrevType = getAbbreviatedMimeType(mimeType); + abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType; + } + + return div( + { className: "requests-menu-subitem requests-menu-type", title: mimeType }, + span({ className: "subitem-label" }, abbrevType) + ); +} + +function TransferredSizeColumn(item) { + const { transferredSize, fromCache, fromServiceWorker } = item; + + let text; + let className = "subitem-label"; + if (fromCache) { + text = L10N.getStr("networkMenu.sizeCached"); + className += " theme-comment"; + } else if (fromServiceWorker) { + text = L10N.getStr("networkMenu.sizeServiceWorker"); + className += " theme-comment"; + } else if (typeof transferredSize == "number") { + text = getFormattedSize(transferredSize); + } else if (transferredSize === null) { + text = L10N.getStr("networkMenu.sizeUnavailable"); + } + + return div( + { className: "requests-menu-subitem requests-menu-transferred", title: text }, + span({ className }, text) + ); +} + +function ContentSizeColumn(item) { + const { contentSize } = item; + + let text; + if (typeof contentSize == "number") { + text = getFormattedSize(contentSize); + } + + return div( + { className: "requests-menu-subitem subitem-label requests-menu-size", title: text }, + span({ className: "subitem-label" }, text) + ); +} + +// List of properties of the timing info we want to create boxes for +const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"]; + +function timingBoxes(item) { + const { eventTimings, totalTime, fromCache, fromServiceWorker } = item; + let boxes = []; + + if (fromCache || fromServiceWorker) { + return boxes; + } + + if (eventTimings) { + // Add a set of boxes representing timing information. + for (let key of TIMING_KEYS) { + let width = eventTimings.timings[key]; + + // Don't render anything if it surely won't be visible. + // One millisecond == one unscaled pixel. + if (width > 0) { + boxes.push(div({ + key, + className: "requests-menu-timings-box " + key, + style: { width } + })); + } + } + } + + if (typeof totalTime == "number") { + let text = L10N.getFormatStr("networkMenu.totalMS", totalTime); + boxes.push(div({ + key: "total", + className: "requests-menu-timings-total", + title: text + }, text)); + } + + return boxes; +} + +function WaterfallColumn(item, firstRequestStartedMillis) { + const startedDeltaMillis = item.startedMillis - firstRequestStartedMillis; + const paddingInlineStart = `${startedDeltaMillis}px`; + + return div({ className: "requests-menu-subitem requests-menu-waterfall" }, + div( + { className: "requests-menu-timings", style: { paddingInlineStart } }, + timingBoxes(item) + ) + ); +} + +module.exports = RequestListItem; diff --git a/devtools/client/netmonitor/components/request-list-tooltip.js b/devtools/client/netmonitor/components/request-list-tooltip.js new file mode 100644 index 000000000000..2bea502cf9e3 --- /dev/null +++ b/devtools/client/netmonitor/components/request-list-tooltip.js @@ -0,0 +1,107 @@ +/* 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/. */ + +/* globals gNetwork, NetMonitorController */ + +"use strict"; + +const { Task } = require("devtools/shared/task"); +const { formDataURI } = require("../request-utils"); +const { WEBCONSOLE_L10N } = require("../l10n"); +const { setImageTooltip, + getImageDimensions } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper"); + +// px +const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; +// px +const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const setTooltipImageContent = Task.async(function* (tooltip, itemEl, requestItem) { + let { mimeType, text, encoding } = requestItem.responseContent.content; + + if (!mimeType || !mimeType.includes("image/")) { + return false; + } + + let string = yield gNetwork.getString(text); + let src = formDataURI(mimeType, encoding, string); + let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM; + let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src); + let options = { maxDim, naturalWidth, naturalHeight }; + setImageTooltip(tooltip, tooltip.doc, src, options); + + return itemEl.querySelector(".requests-menu-icon"); +}); + +const setTooltipStackTraceContent = Task.async(function* (tooltip, requestItem) { + let {stacktrace} = requestItem.cause; + + if (!stacktrace || stacktrace.length == 0) { + return false; + } + + let doc = tooltip.doc; + let el = doc.createElementNS(HTML_NS, "div"); + el.className = "stack-trace-tooltip devtools-monospace"; + + for (let f of stacktrace) { + let { functionName, filename, lineNumber, columnNumber, asyncCause } = f; + + if (asyncCause) { + // if there is asyncCause, append a "divider" row into the trace + let asyncFrameEl = doc.createElementNS(HTML_NS, "div"); + asyncFrameEl.className = "stack-frame stack-frame-async"; + asyncFrameEl.textContent = + WEBCONSOLE_L10N.getFormatStr("stacktrace.asyncStack", asyncCause); + el.appendChild(asyncFrameEl); + } + + // Parse a source name in format "url -> url" + let sourceUrl = filename.split(" -> ").pop(); + + let frameEl = doc.createElementNS(HTML_NS, "div"); + frameEl.className = "stack-frame stack-frame-call"; + + let funcEl = doc.createElementNS(HTML_NS, "span"); + funcEl.className = "stack-frame-function-name"; + funcEl.textContent = + functionName || WEBCONSOLE_L10N.getStr("stacktrace.anonymousFunction"); + frameEl.appendChild(funcEl); + + let sourceEl = doc.createElementNS(HTML_NS, "span"); + sourceEl.className = "stack-frame-source-name"; + frameEl.appendChild(sourceEl); + + let sourceInnerEl = doc.createElementNS(HTML_NS, "span"); + sourceInnerEl.className = "stack-frame-source-name-inner"; + sourceEl.appendChild(sourceInnerEl); + + sourceInnerEl.textContent = sourceUrl; + sourceInnerEl.title = sourceUrl; + + let lineEl = doc.createElementNS(HTML_NS, "span"); + lineEl.className = "stack-frame-line"; + lineEl.textContent = `:${lineNumber}:${columnNumber}`; + sourceInnerEl.appendChild(lineEl); + + frameEl.addEventListener("click", () => { + // hide the tooltip immediately, not after delay + tooltip.hide(); + NetMonitorController.viewSourceInDebugger(filename, lineNumber); + }, false); + + el.appendChild(frameEl); + } + + tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH}); + + return true; +}); + +module.exports = { + setTooltipImageContent, + setTooltipStackTraceContent, +}; diff --git a/devtools/client/netmonitor/components/request-list.js b/devtools/client/netmonitor/components/request-list.js new file mode 100644 index 000000000000..7048fdb4b629 --- /dev/null +++ b/devtools/client/netmonitor/components/request-list.js @@ -0,0 +1,34 @@ +/* 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 { createFactory, PropTypes, DOM } = require("devtools/client/shared/vendor/react"); +const { div } = DOM; +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const RequestListHeader = createFactory(require("./request-list-header")); +const RequestListEmptyNotice = createFactory(require("./request-list-empty")); +const RequestListContent = createFactory(require("./request-list-content")); + +/** + * Renders the request list - header, empty text, the actual content with rows + */ +const RequestList = function ({ isEmpty }) { + return div({ className: "request-list-container" }, + RequestListHeader(), + isEmpty ? RequestListEmptyNotice() : RequestListContent() + ); +}; + +RequestList.displayName = "RequestList"; + +RequestList.propTypes = { + isEmpty: PropTypes.bool.isRequired, +}; + +module.exports = connect( + state => ({ + isEmpty: state.requests.requests.isEmpty() + }) +)(RequestList); diff --git a/devtools/client/netmonitor/components/shared/moz.build b/devtools/client/netmonitor/components/shared/moz.build new file mode 100644 index 000000000000..ce44aaec6a88 --- /dev/null +++ b/devtools/client/netmonitor/components/shared/moz.build @@ -0,0 +1,7 @@ +# 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/. + +DevToolsModules( + 'timings-panel.js', +) diff --git a/devtools/client/netmonitor/components/shared/timings-panel.js b/devtools/client/netmonitor/components/shared/timings-panel.js new file mode 100644 index 000000000000..49924701891f --- /dev/null +++ b/devtools/client/netmonitor/components/shared/timings-panel.js @@ -0,0 +1,85 @@ +/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { L10N } = require("../../l10n"); +const { getSelectedRequest } = require("../../selectors/index"); + +const { div, span } = DOM; +const types = ["blocked", "dns", "connect", "send", "wait", "receive"]; +const TIMINGS_END_PADDING = "80px"; + +/* + * Timings panel component + * Display timeline bars that shows the total wait time for various stages + */ +function TimingsPanel({ + timings = {}, + totalTime = 0, +}) { + const timelines = types.map((type, idx) => { + // Determine the relative offset for each timings box. For example, the + // offset of third timings box will be 0 + blocked offset + dns offset + const offset = types + .slice(0, idx) + .reduce((acc, cur) => (acc + timings[cur] || 0), 0); + const offsetScale = offset / totalTime || 0; + const timelineScale = timings[type] / totalTime || 0; + + return div({ + key: type, + id: `timings-summary-${type}`, + className: "tabpanel-summary-container", + }, + span({ className: "tabpanel-summary-label" }, + L10N.getStr(`netmonitor.timings.${type}`) + ), + div({ className: "requests-menu-timings-container" }, + span({ + className: "requests-menu-timings-offset", + style: { + width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`, + }, + }), + span({ + className: `requests-menu-timings-box ${type}`, + style: { + width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`, + }, + }), + span({ className: "requests-menu-timings-total" }, + L10N.getFormatStr("networkMenu.totalMS", timings[type]) + ) + ), + ); + }); + + return div({}, timelines); +} + +TimingsPanel.displayName = "TimingsPanel"; + +TimingsPanel.propTypes = { + timings: PropTypes.object, + totalTime: PropTypes.number, +}; + +module.exports = connect( + (state) => { + const selectedRequest = getSelectedRequest(state); + + if (selectedRequest && selectedRequest.eventTimings) { + const { timings, totalTime } = selectedRequest.eventTimings; + return { + timings, + totalTime, + }; + } + + return {}; + } +)(TimingsPanel); diff --git a/devtools/client/netmonitor/components/summary-button.js b/devtools/client/netmonitor/components/summary-button.js index 9465d3147b6c..7b8980d4ec5c 100644 --- a/devtools/client/netmonitor/components/summary-button.js +++ b/devtools/client/netmonitor/components/summary-button.js @@ -14,7 +14,7 @@ const { DOM, PropTypes } = require("devtools/client/shared/vendor/react"); const { connect } = require("devtools/client/shared/vendor/react-redux"); const { PluralForm } = require("devtools/shared/plural-form"); const { L10N } = require("../l10n"); -const { getSummary } = require("../selectors/index"); +const { getDisplayedRequestsSummary } = require("../selectors/index"); const { button, span } = DOM; @@ -22,14 +22,12 @@ function SummaryButton({ summary, triggerSummary, }) { - let { count, totalBytes, totalMillis } = summary; + let { count, bytes, millis } = summary; const text = (count === 0) ? L10N.getStr("networkMenu.empty") : PluralForm.get(count, L10N.getStr("networkMenu.summary")) .replace("#1", count) - .replace("#2", L10N.numberWithDecimals(totalBytes / 1024, - CONTENT_SIZE_DECIMALS)) - .replace("#3", L10N.numberWithDecimals(totalMillis / 1000, - REQUEST_TIME_DECIMALS)); + .replace("#2", L10N.numberWithDecimals(bytes / 1024, CONTENT_SIZE_DECIMALS)) + .replace("#3", L10N.numberWithDecimals(millis / 1000, REQUEST_TIME_DECIMALS)); return button({ id: "requests-menu-network-summary-button", @@ -47,7 +45,7 @@ SummaryButton.propTypes = { module.exports = connect( (state) => ({ - summary: getSummary(state), + summary: getDisplayedRequestsSummary(state), }), (dispatch) => ({ triggerSummary: () => { diff --git a/devtools/client/netmonitor/components/toggle-button.js b/devtools/client/netmonitor/components/toggle-button.js index 2a3cea027529..57d7f3a924e1 100644 --- a/devtools/client/netmonitor/components/toggle-button.js +++ b/devtools/client/netmonitor/components/toggle-button.js @@ -2,21 +2,20 @@ * 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/. */ -/* globals NetMonitorView */ - "use strict"; const { DOM, PropTypes } = require("devtools/client/shared/vendor/react"); const { connect } = require("devtools/client/shared/vendor/react-redux"); const { L10N } = require("../l10n"); const Actions = require("../actions/index"); +const { isSidebarToggleButtonDisabled } = require("../selectors/index"); const { button } = DOM; function ToggleButton({ disabled, open, - triggerSidebar, + onToggle, }) { let className = ["devtools-button"]; if (!open) { @@ -32,34 +31,21 @@ function ToggleButton({ title, disabled, tabIndex: "0", - onMouseDown: triggerSidebar, + onMouseDown: onToggle, }); } ToggleButton.propTypes = { disabled: PropTypes.bool.isRequired, - triggerSidebar: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, }; module.exports = connect( (state) => ({ - disabled: state.requests.items.length === 0, - open: state.ui.sidebar.open, + disabled: isSidebarToggleButtonDisabled(state), + open: state.ui.sidebarOpen, }), (dispatch) => ({ - triggerSidebar: () => { - dispatch(Actions.toggleSidebar()); - - let requestsMenu = NetMonitorView.RequestsMenu; - let selectedIndex = requestsMenu.selectedIndex; - - // Make sure there's a selection if the button is pressed, to avoid - // showing an empty network details pane. - if (selectedIndex == -1 && requestsMenu.itemCount) { - requestsMenu.selectedIndex = 0; - } else { - requestsMenu.selectedIndex = -1; - } - }, + onToggle: () => dispatch(Actions.toggleSidebar()) }) )(ToggleButton); diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js index 48f520e64401..de98cf7c29d0 100644 --- a/devtools/client/netmonitor/constants.js +++ b/devtools/client/netmonitor/constants.js @@ -11,12 +11,40 @@ const general = { }; const actionTypes = { + BATCH_ACTIONS: "BATCH_ACTIONS", + BATCH_ENABLE: "BATCH_ENABLE", + ADD_TIMING_MARKER: "ADD_TIMING_MARKER", + CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS", + ADD_REQUEST: "ADD_REQUEST", + UPDATE_REQUEST: "UPDATE_REQUEST", + CLEAR_REQUESTS: "CLEAR_REQUESTS", + CLONE_SELECTED_REQUEST: "CLONE_SELECTED_REQUEST", + REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST", + SELECT_REQUEST: "SELECT_REQUEST", + PRESELECT_REQUEST: "PRESELECT_REQUEST", + SORT_BY: "SORT_BY", TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE", ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY", SET_FILTER_TEXT: "SET_FILTER_TEXT", OPEN_SIDEBAR: "OPEN_SIDEBAR", - TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR", - UPDATE_REQUESTS: "UPDATE_REQUESTS", + WATERFALL_RESIZE: "WATERFALL_RESIZE", }; -module.exports = Object.assign({}, general, actionTypes); +// Descriptions for what this frontend is currently doing. +const ACTIVITY_TYPE = { + // Standing by and handling requests normally. + NONE: 0, + + // Forcing the target to reload with cache enabled or disabled. + RELOAD: { + WITH_CACHE_ENABLED: 1, + WITH_CACHE_DISABLED: 2, + WITH_CACHE_DEFAULT: 3 + }, + + // Enabling or disabling the cache without triggering a reload. + ENABLE_CACHE: 3, + DISABLE_CACHE: 4 +}; + +module.exports = Object.assign({ ACTIVITY_TYPE }, general, actionTypes); diff --git a/devtools/client/netmonitor/custom-request-view.js b/devtools/client/netmonitor/custom-request-view.js index 06871a61f483..f7ebf27e3d13 100644 --- a/devtools/client/netmonitor/custom-request-view.js +++ b/devtools/client/netmonitor/custom-request-view.js @@ -7,12 +7,11 @@ "use strict"; const { Task } = require("devtools/shared/task"); -const { - writeHeaderText, - getKeyWithEvent, - getUrlQuery, - parseQueryString, -} = require("./request-utils"); +const { writeHeaderText, + getKeyWithEvent, + getUrlQuery, + parseQueryString } = require("./request-utils"); +const Actions = require("./actions/index"); /** * Functions handling the custom request view. @@ -76,37 +75,41 @@ CustomRequestView.prototype = { */ onUpdate: function (field) { let selectedItem = NetMonitorView.RequestsMenu.selectedItem; + let store = NetMonitorView.RequestsMenu.store; let value; switch (field) { case "method": value = $("#custom-method-value").value.trim(); - selectedItem.attachment.method = value; + store.dispatch(Actions.updateRequest(selectedItem.id, { method: value })); break; case "url": value = $("#custom-url-value").value; this.updateCustomQuery(value); - selectedItem.attachment.url = value; + store.dispatch(Actions.updateRequest(selectedItem.id, { url: value })); break; case "query": let query = $("#custom-query-value").value; this.updateCustomUrl(query); - field = "url"; value = $("#custom-url-value").value; - selectedItem.attachment.url = value; + store.dispatch(Actions.updateRequest(selectedItem.id, { url: value })); break; case "body": value = $("#custom-postdata-value").value; - selectedItem.attachment.requestPostData = { postData: { text: value } }; + store.dispatch(Actions.updateRequest(selectedItem.id, { + requestPostData: { + postData: { text: value } + } + })); break; case "headers": let headersText = $("#custom-headers-value").value; value = parseHeadersText(headersText); - selectedItem.attachment.requestHeaders = { headers: value }; + store.dispatch(Actions.updateRequest(selectedItem.id, { + requestHeaders: { headers: value } + })); break; } - - NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); }, /** @@ -161,7 +164,7 @@ function parseHeadersText(text) { * Parse readable text list of a query string. * * @param string text - * Text of query string represetation + * Text of query string representation * @return array * Array of query params {name, value} */ diff --git a/devtools/client/netmonitor/details-view.js b/devtools/client/netmonitor/details-view.js index 97687323a131..9d531f11a781 100644 --- a/devtools/client/netmonitor/details-view.js +++ b/devtools/client/netmonitor/details-view.js @@ -2,14 +2,14 @@ * 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/. */ -/* import-globals-from ./netmonitor-controller.js */ /* eslint-disable mozilla/reject-some-requires */ -/* globals dumpn, $, NetMonitorView, gNetwork */ +/* globals window, dumpn, $, NetMonitorView, gNetwork */ "use strict"; const promise = require("promise"); const EventEmitter = require("devtools/shared/event-emitter"); +const Editor = require("devtools/client/sourceeditor/editor"); const { Heritage } = require("devtools/client/shared/widgets/view-helpers"); const { Task } = require("devtools/shared/task"); const { ToolSidebar } = require("devtools/client/framework/sidebar"); @@ -27,6 +27,10 @@ const { getUrlHost, parseQueryString, } = require("./request-utils"); +const { createFactory } = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); +const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider); +const TimingsPanel = createFactory(require("./components/shared/timings-panel")); // 100 KB in bytes const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; @@ -86,9 +90,16 @@ DetailsView.prototype = { /** * Initialization function, called when the network monitor is started. */ - initialize: function () { + initialize: function (store) { dumpn("Initializing the DetailsView"); + this._timingsPanelNode = $("#react-timings-tabpanel-hook"); + + ReactDOM.render(Provider( + { store }, + TimingsPanel() + ), this._timingsPanelNode); + this.widget = $("#event-details-pane"); this.sidebar = new ToolSidebar(this.widget, this, "netmonitor", { disableTelemetry: true, @@ -134,6 +145,7 @@ DetailsView.prototype = { */ destroy: function () { dumpn("Destroying the DetailsView"); + ReactDOM.unmountComponentAtNode(this._timingsPanelNode); this.sidebar.destroy(); $("tabpanels", this.widget).removeEventListener("select", this._onTabSelect); @@ -243,10 +255,6 @@ DetailsView.prototype = { case 3: yield view._setResponseBody(src.url, src.responseContent); break; - // "Timings" - case 4: - yield view._setTimingsInformation(src.eventTimings); - break; // "Security" case 5: yield view._setSecurityInfo(src.securityInfo, src.url); @@ -267,10 +275,6 @@ DetailsView.prototype = { // Tab is selected but not dirty. We're done here. populated[tab] = true; window.emit(EVENTS.TAB_UPDATED); - - if (NetMonitorController.isConnected()) { - NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible(); - } } } else if (viewState.dirty[tab]) { // Tab is dirty but no longer selected. Don't refresh it now, it'll be @@ -328,7 +332,7 @@ DetailsView.prototype = { } else { code = data.status; } - $("#headers-summary-status-circle").setAttribute("code", code); + $("#headers-summary-status-circle").setAttribute("data-code", code); $("#headers-summary-status-value").setAttribute("value", data.status + " " + data.statusText); $("#headers-summary-status").removeAttribute("hidden"); @@ -689,87 +693,6 @@ DetailsView.prototype = { window.emit(EVENTS.RESPONSE_BODY_DISPLAYED); }), - /** - * Sets the timings information shown in this view. - * - * @param object response - * The message received from the server. - */ - _setTimingsInformation: function (response) { - if (!response) { - return; - } - let { blocked, dns, connect, send, wait, receive } = response.timings; - - let tabboxWidth = $("#details-pane").getAttribute("width"); - - // Other nodes also take some space. - let availableWidth = tabboxWidth / 2; - let scale = (response.totalTime > 0 ? - Math.max(availableWidth / response.totalTime, 0) : - 0); - - $("#timings-summary-blocked .requests-menu-timings-box") - .setAttribute("width", blocked * scale); - $("#timings-summary-blocked .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", blocked)); - - $("#timings-summary-dns .requests-menu-timings-box") - .setAttribute("width", dns * scale); - $("#timings-summary-dns .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", dns)); - - $("#timings-summary-connect .requests-menu-timings-box") - .setAttribute("width", connect * scale); - $("#timings-summary-connect .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect)); - - $("#timings-summary-send .requests-menu-timings-box") - .setAttribute("width", send * scale); - $("#timings-summary-send .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", send)); - - $("#timings-summary-wait .requests-menu-timings-box") - .setAttribute("width", wait * scale); - $("#timings-summary-wait .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", wait)); - - $("#timings-summary-receive .requests-menu-timings-box") - .setAttribute("width", receive * scale); - $("#timings-summary-receive .requests-menu-timings-total") - .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", receive)); - - $("#timings-summary-dns .requests-menu-timings-box") - .style.transform = "translateX(" + (scale * blocked) + "px)"; - $("#timings-summary-connect .requests-menu-timings-box") - .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; - $("#timings-summary-send .requests-menu-timings-box") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect)) + "px)"; - $("#timings-summary-wait .requests-menu-timings-box") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; - $("#timings-summary-receive .requests-menu-timings-box") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect + send + wait)) + - "px)"; - - $("#timings-summary-dns .requests-menu-timings-total") - .style.transform = "translateX(" + (scale * blocked) + "px)"; - $("#timings-summary-connect .requests-menu-timings-total") - .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; - $("#timings-summary-send .requests-menu-timings-total") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect)) + "px)"; - $("#timings-summary-wait .requests-menu-timings-total") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect + send)) + "px)"; - $("#timings-summary-receive .requests-menu-timings-total") - .style.transform = - "translateX(" + (scale * (blocked + dns + connect + send + wait)) + - "px)"; - }, - /** * Sets the preview for HTML responses shown in this view. * diff --git a/devtools/client/netmonitor/har/har-builder.js b/devtools/client/netmonitor/har/har-builder.js index 565a4be0973c..698be8dcfd01 100644 --- a/devtools/client/netmonitor/har/har-builder.js +++ b/devtools/client/netmonitor/har/har-builder.js @@ -63,9 +63,7 @@ HarBuilder.prototype = { let log = this.buildLog(); // Build entries. - let items = this._options.items; - for (let i = 0; i < items.length; i++) { - let file = items[i].attachment; + for (let file of this._options.items) { log.entries.push(this.buildEntry(log, file)); } diff --git a/devtools/client/netmonitor/har/har-collector.js b/devtools/client/netmonitor/har/har-collector.js index a55326a51e16..315066bf7572 100644 --- a/devtools/client/netmonitor/har/har-collector.js +++ b/devtools/client/netmonitor/har/har-collector.js @@ -201,9 +201,7 @@ HarCollector.prototype = { this.files.set(actor, file); // Mimic the Net panel data structure - this.items.push({ - attachment: file - }); + this.items.push(file); }, onNetworkEventUpdate: function (type, packet) { diff --git a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js index 882088cd19bd..564eea991f99 100644 --- a/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js +++ b/devtools/client/netmonitor/har/test/browser_net_har_throttle_upload.js @@ -16,7 +16,7 @@ function* throttleUploadTest(actuallyThrottle) { info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")"); - let { NetMonitorView } = monitor.panelWin; + let { NetMonitorController, NetMonitorView } = monitor.panelWin; let { RequestsMenu } = NetMonitorView; const size = 4096; @@ -32,7 +32,7 @@ function* throttleUploadTest(actuallyThrottle) { uploadBPSMax: uploadSize, }, }; - let client = monitor._controller.webConsoleClient; + let client = NetMonitorController.webConsoleClient; info("sending throttle request"); let deferred = promise.defer(); diff --git a/devtools/client/netmonitor/middleware/batching.js b/devtools/client/netmonitor/middleware/batching.js new file mode 100644 index 000000000000..bf53d787bb0c --- /dev/null +++ b/devtools/client/netmonitor/middleware/batching.js @@ -0,0 +1,132 @@ +/* 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 { BATCH_ACTIONS, BATCH_ENABLE, BATCH_RESET } = require("../constants"); + +// ms +const REQUESTS_REFRESH_RATE = 50; + +/** + * Middleware that watches for actions with a "batch = true" value in their meta field. + * These actions are queued and dispatched as one batch after a timeout. + * Special actions that are handled by this middleware: + * - BATCH_ENABLE can be used to enable and disable the batching. + * - BATCH_RESET discards the actions that are currently in the queue. + */ +function batchingMiddleware(store) { + return next => { + let queuedActions = []; + let enabled = true; + let flushTask = null; + + return action => { + if (action.type === BATCH_ENABLE) { + return setEnabled(action.enabled); + } + + if (action.type === BATCH_RESET) { + return resetQueue(); + } + + if (action.meta && action.meta.batch) { + if (!enabled) { + next(action); + return Promise.resolve(); + } + + queuedActions.push(action); + + if (!flushTask) { + flushTask = new DelayedTask(flushActions, REQUESTS_REFRESH_RATE); + } + + return flushTask.promise; + } + + return next(action); + }; + + function setEnabled(value) { + enabled = value; + + // If disabling the batching, flush the actions that have been queued so far + if (!enabled && flushTask) { + flushTask.runNow(); + } + } + + function resetQueue() { + queuedActions = []; + + if (flushTask) { + flushTask.cancel(); + flushTask = null; + } + } + + function flushActions() { + const actions = queuedActions; + queuedActions = []; + + next({ + type: BATCH_ACTIONS, + actions, + }); + + flushTask = null; + } + }; +} + +/** + * Create a delayed task that calls the specified task function after a delay. + */ +function DelayedTask(taskFn, delay) { + this._promise = new Promise((resolve, reject) => { + this.runTask = (cancel) => { + if (cancel) { + reject("Task cancelled"); + } else { + taskFn(); + resolve(); + } + this.runTask = null; + }; + this.timeout = setTimeout(this.runTask, delay); + }).catch(console.error); +} + +DelayedTask.prototype = { + /** + * Return a promise that is resolved after the task is performed or canceled. + */ + get promise() { + return this._promise; + }, + + /** + * Cancel the execution of the task. + */ + cancel() { + clearTimeout(this.timeout); + if (this.runTask) { + this.runTask(true); + } + }, + + /** + * Execute the scheduled task immediately, without waiting for the timeout. + * Resolves the promise correctly. + */ + runNow() { + clearTimeout(this.timeout); + if (this.runTask) { + this.runTask(); + } + } +}; + +module.exports = batchingMiddleware; diff --git a/devtools/client/netmonitor/middleware/moz.build b/devtools/client/netmonitor/middleware/moz.build new file mode 100644 index 000000000000..19c8f8b69de3 --- /dev/null +++ b/devtools/client/netmonitor/middleware/moz.build @@ -0,0 +1,7 @@ +# 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/. + +DevToolsModules( + 'batching.js', +) diff --git a/devtools/client/netmonitor/moz.build b/devtools/client/netmonitor/moz.build index b80c8726a458..107316ebe851 100644 --- a/devtools/client/netmonitor/moz.build +++ b/devtools/client/netmonitor/moz.build @@ -6,8 +6,10 @@ DIRS += [ 'actions', 'components', 'har', + 'middleware', 'reducers', - 'selectors' + 'selectors', + 'utils', ] DevToolsModules( @@ -17,6 +19,8 @@ DevToolsModules( 'events.js', 'filter-predicates.js', 'l10n.js', + 'netmonitor-controller.js', + 'netmonitor-view.js', 'panel.js', 'performance-statistics-view.js', 'prefs.js', @@ -27,6 +31,7 @@ DevToolsModules( 'sort-predicates.js', 'store.js', 'toolbar-view.js', + 'waterfall-background.js', ) BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 2cd595a30d9d..5017898c9493 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -3,37 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-disable mozilla/reject-some-requires */ -/* globals window, document, NetMonitorView, gStore, Actions */ -/* exported loader */ +/* globals window, NetMonitorView, gStore, dumpn */ "use strict"; -var { utils: Cu } = Components; - -// Descriptions for what this frontend is currently doing. -const ACTIVITY_TYPE = { - // Standing by and handling requests normally. - NONE: 0, - - // Forcing the target to reload with cache enabled or disabled. - RELOAD: { - WITH_CACHE_ENABLED: 1, - WITH_CACHE_DISABLED: 2, - WITH_CACHE_DEFAULT: 3 - }, - - // Enabling or disabling the cache without triggering a reload. - ENABLE_CACHE: 3, - DISABLE_CACHE: 4 -}; - -var BrowserLoaderModule = {}; -Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule); -var { loader, require } = BrowserLoaderModule.BrowserLoader({ - baseURI: "resource://devtools/client/netmonitor/", - window -}); - const promise = require("promise"); const Services = require("Services"); const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); @@ -41,20 +14,22 @@ const EventEmitter = require("devtools/shared/event-emitter"); const Editor = require("devtools/client/sourceeditor/editor"); const {TimelineFront} = require("devtools/shared/fronts/timeline"); const {Task} = require("devtools/shared/task"); -const {Prefs} = require("./prefs"); -const {EVENTS} = require("./events"); +const { ACTIVITY_TYPE } = require("./constants"); +const { EVENTS } = require("./events"); +const { configureStore } = require("./store"); const Actions = require("./actions/index"); +const { getDisplayedRequestById } = require("./selectors/index"); +const { Prefs } = require("./prefs"); -XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); -XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE); -XPCOMUtils.defineConstant(this, "Editor", Editor); -XPCOMUtils.defineConstant(this, "Prefs", Prefs); - -XPCOMUtils.defineLazyModuleGetter(this, "Chart", +XPCOMUtils.defineConstant(window, "EVENTS", EVENTS); +XPCOMUtils.defineConstant(window, "ACTIVITY_TYPE", ACTIVITY_TYPE); +XPCOMUtils.defineConstant(window, "Editor", Editor); +XPCOMUtils.defineConstant(window, "Prefs", Prefs); +XPCOMUtils.defineLazyModuleGetter(window, "Chart", "resource://devtools/client/shared/widgets/Chart.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", - "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); +// Initialize the global Redux store +window.gStore = configureStore(); /** * Object defining the network monitor controller components. @@ -91,6 +66,7 @@ var NetMonitorController = { } this._shutdown = promise.defer(); { + gStore.dispatch(Actions.batchReset()); NetMonitorView.destroy(); this.TargetEventsHandler.disconnect(); this.NetworkEventsHandler.disconnect(); @@ -287,19 +263,18 @@ var NetMonitorController = { let deferred = promise.defer(); let request = null; let inspector = function () { - let predicate = i => i.value === requestId; - request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate); + request = getDisplayedRequestById(gStore.getState(), requestId); if (!request) { // Reset filters so that the request is visible. gStore.dispatch(Actions.toggleFilterType("all")); - request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate); + request = getDisplayedRequestById(gStore.getState(), requestId); } // If the request was found, select it. Otherwise this function will be // called again once new requests arrive. if (request) { window.off(EVENTS.REQUEST_ADDED, inspector); - NetMonitorView.RequestsMenu.selectedItem = request; + gStore.dispatch(Actions.selectRequest(request.id)); deferred.resolve(); } }; @@ -398,14 +373,14 @@ TargetEventsHandler.prototype = { // Reset UI. if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) { NetMonitorView.RequestsMenu.reset(); - NetMonitorView.Sidebar.toggle(false); + } else { + // If the log is persistent, just clear all accumulated timing markers. + gStore.dispatch(Actions.clearTimingMarkers()); } // Switch to the default network traffic inspector view. if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) { NetMonitorView.showNetworkInspectorView(); } - // Clear any accumulated markers. - NetMonitorController.NetworkEventsHandler.clearMarkers(); window.emit(EVENTS.TARGET_WILL_NAVIGATE); break; @@ -429,8 +404,6 @@ 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); @@ -456,19 +429,6 @@ NetworkEventsHandler.prototype = { return NetMonitorController.timelineFront; }, - get firstDocumentDOMContentLoadedTimestamp() { - let marker = this._markers.filter(e => { - return 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. */ @@ -525,7 +485,7 @@ NetworkEventsHandler.prototype = { */ _onDocLoadingMarker: function (marker) { window.emit(EVENTS.TIMELINE_EVENT, marker); - this._markers.push(marker); + gStore.dispatch(Actions.addTimingMarker(marker)); }, /** @@ -547,8 +507,7 @@ NetworkEventsHandler.prototype = { } = networkInfo; NetMonitorView.RequestsMenu.addRequest( - actor, startedDateTime, method, url, isXHR, cause, fromCache, - fromServiceWorker + actor, {startedDateTime, method, url, isXHR, cause, fromCache, fromServiceWorker} ); window.emit(EVENTS.NETWORK_EVENT, actor); }, @@ -637,7 +596,7 @@ NetworkEventsHandler.prototype = { _onRequestHeaders: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { requestHeaders: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from); }); }, @@ -651,7 +610,7 @@ NetworkEventsHandler.prototype = { _onRequestCookies: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { requestCookies: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from); }); }, @@ -665,7 +624,7 @@ NetworkEventsHandler.prototype = { _onRequestPostData: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { requestPostData: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from); }); }, @@ -679,7 +638,7 @@ NetworkEventsHandler.prototype = { _onSecurityInfo: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { securityInfo: response.securityInfo - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from); }); }, @@ -693,7 +652,7 @@ NetworkEventsHandler.prototype = { _onResponseHeaders: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { responseHeaders: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from); }); }, @@ -707,7 +666,7 @@ NetworkEventsHandler.prototype = { _onResponseCookies: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { responseCookies: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from); }); }, @@ -721,7 +680,7 @@ NetworkEventsHandler.prototype = { _onResponseContent: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { responseContent: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from); }); }, @@ -735,18 +694,11 @@ NetworkEventsHandler.prototype = { _onEventTimings: function (response) { NetMonitorView.RequestsMenu.updateRequest(response.from, { eventTimings: response - }, () => { + }).then(() => { window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from); }); }, - /** - * Clears all accumulated markers. - */ - clearMarkers: function () { - this._markers.length = 0; - }, - /** * Fetches the full text of a LongString. * @@ -763,19 +715,10 @@ NetworkEventsHandler.prototype = { } }; -/** - * Returns true if this is document is in RTL mode. - * @return boolean - */ -XPCOMUtils.defineLazyGetter(window, "isRTL", function () { - return window.getComputedStyle(document.documentElement, null) - .direction == "rtl"; -}); - /** * Convenient way of emitting events from the panel window. */ -EventEmitter.decorate(this); +EventEmitter.decorate(window); /** * Preliminary setup for the NetMonitorController object. @@ -795,14 +738,4 @@ Object.defineProperties(window, { } }); -/** - * Helper method for debugging. - * @param string - */ -function dumpn(str) { - if (wantLogging) { - dump("NET-FRONTEND: " + str + "\n"); - } -} - -var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +exports.NetMonitorController = NetMonitorController; diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index b13b3f91df14..84461e406021 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -2,25 +2,25 @@ * 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/. */ -/* import-globals-from ./netmonitor-controller.js */ /* eslint-disable mozilla/reject-some-requires */ -/* globals Prefs, setInterval, setTimeout, clearInterval, clearTimeout, btoa */ -/* exported $, $all */ +/* globals $, gStore, NetMonitorController, dumpn */ "use strict"; const { testing: isTesting } = require("devtools/shared/flags"); +const promise = require("promise"); +const Editor = require("devtools/client/sourceeditor/editor"); +const { Task } = require("devtools/shared/task"); const { ViewHelpers } = require("devtools/client/shared/widgets/view-helpers"); -const { configureStore } = require("./store"); const { RequestsMenuView } = require("./requests-menu-view"); const { CustomRequestView } = require("./custom-request-view"); const { ToolbarView } = require("./toolbar-view"); const { SidebarView } = require("./sidebar-view"); const { DetailsView } = require("./details-view"); const { PerformanceStatisticsView } = require("./performance-statistics-view"); - -// Initialize the global redux variables -var gStore = configureStore(); +const { ACTIVITY_TYPE } = require("./constants"); +const Actions = require("./actions/index"); +const { Prefs } = require("./prefs"); // ms const WDA_DEFAULT_VERIFY_INTERVAL = 50; @@ -50,7 +50,7 @@ var NetMonitorView = { this.Toolbar.initialize(gStore); this.RequestsMenu.initialize(gStore); - this.NetworkDetails.initialize(); + this.NetworkDetails.initialize(gStore); this.CustomRequest.initialize(); this.PerformanceStatistics.initialize(gStore); }, @@ -80,12 +80,6 @@ var NetMonitorView = { this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth); this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight); this.toggleDetailsPane({ visible: false }); - - // Disable the performance statistics mode. - if (!Prefs.statistics) { - $("#request-menu-context-perf").hidden = true; - $("#notice-perf-message").hidden = true; - } }, /** @@ -169,7 +163,6 @@ var NetMonitorView = { */ showNetworkInspectorView: function () { this._body.selectedPanel = $("#network-inspector-view"); - this.RequestsMenu._flushWaterfallViews(true); }, /** @@ -192,7 +185,7 @@ var NetMonitorView = { // • The response content size and request total time are necessary for // populating the statistics view. // • The response mime type is used for categorization. - yield whenDataAvailable(requestsView, [ + yield whenDataAvailable(requestsView.store, [ "responseHeaders", "status", "contentSize", "mimeType", "totalTime" ]); } catch (ex) { @@ -200,8 +193,9 @@ var NetMonitorView = { console.error(ex); } - statisticsView.createPrimedCacheChart(requestsView.items); - statisticsView.createEmptyCacheChart(requestsView.items); + const requests = requestsView.store.getState().requests.requests; + statisticsView.createPrimedCacheChart(requests); + statisticsView.createEmptyCacheChart(requests); }); }, @@ -241,44 +235,39 @@ var NetMonitorView = { _editorPromises: new Map() }; -/** - * DOM query helper. - * TODO: Move it into "dom-utils.js" module and "require" it when needed. - */ -var $ = (selector, target = document) => target.querySelector(selector); -var $all = (selector, target = document) => target.querySelectorAll(selector); - /** * Makes sure certain properties are available on all objects in a data store. * - * @param array dataStore - * The request view object from which to fetch the item list. + * @param Store dataStore + * A Redux store for which to check the availability of properties. * @param array mandatoryFields * A list of strings representing properties of objects in dataStore. * @return object * A promise resolved when all objects in dataStore contain the * properties defined in mandatoryFields. */ -function whenDataAvailable(requestsView, mandatoryFields) { - let deferred = promise.defer(); +function whenDataAvailable(dataStore, mandatoryFields) { + return new Promise((resolve, reject) => { + let interval = setInterval(() => { + const { requests } = dataStore.getState().requests; + const allFieldsPresent = !requests.isEmpty() && requests.every( + item => mandatoryFields.every( + field => item.get(field) !== undefined + ) + ); - let interval = setInterval(() => { - const { attachments } = requestsView; - if (attachments.length > 0 && attachments.every(item => { - return mandatoryFields.every(field => field in item); - })) { + if (allFieldsPresent) { + clearInterval(interval); + clearTimeout(timer); + resolve(); + } + }, WDA_DEFAULT_VERIFY_INTERVAL); + + let timer = setTimeout(() => { clearInterval(interval); - clearTimeout(timer); - deferred.resolve(); - } - }, WDA_DEFAULT_VERIFY_INTERVAL); - - let timer = setTimeout(() => { - clearInterval(interval); - deferred.reject(new Error("Timed out while waiting for data")); - }, WDA_DEFAULT_GIVE_UP_TIMEOUT); - - return deferred.promise; + reject(new Error("Timed out while waiting for data")); + }, WDA_DEFAULT_GIVE_UP_TIMEOUT); + }); } /** @@ -290,3 +279,5 @@ NetMonitorView.NetworkDetails = new DetailsView(); NetMonitorView.RequestsMenu = new RequestsMenuView(); NetMonitorView.CustomRequest = new CustomRequestView(); NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView(); + +exports.NetMonitorView = NetMonitorView; diff --git a/devtools/client/netmonitor/netmonitor.js b/devtools/client/netmonitor/netmonitor.js new file mode 100644 index 000000000000..5c23b5c52657 --- /dev/null +++ b/devtools/client/netmonitor/netmonitor.js @@ -0,0 +1,60 @@ +/* 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/. */ + +/* globals window, document, NetMonitorController, NetMonitorView */ +/* exported Netmonitor, NetMonitorController, NetMonitorView, $, $all, dumpn */ + +"use strict"; + +const Cu = Components.utils; +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); + +function Netmonitor(toolbox) { + const { require } = BrowserLoader({ + baseURI: "resource://devtools/client/netmonitor/", + window, + commonLibRequire: toolbox.browserRequire, + }); + + window.windowRequire = require; + + const { NetMonitorController } = require("./netmonitor-controller.js"); + const { NetMonitorView } = require("./netmonitor-view.js"); + + window.NetMonitorController = NetMonitorController; + window.NetMonitorView = NetMonitorView; + + NetMonitorController._toolbox = toolbox; + NetMonitorController._target = toolbox.target; +} + +Netmonitor.prototype = { + init() { + return window.NetMonitorController.startupNetMonitor(); + }, + + destroy() { + return window.NetMonitorController.shutdownNetMonitor(); + } +}; + +/** + * DOM query helper. + * TODO: Move it into "dom-utils.js" module and "require" it when needed. + */ +var $ = (selector, target = document) => target.querySelector(selector); +var $all = (selector, target = document) => target.querySelectorAll(selector); + +/** + * Helper method for debugging. + * @param string + */ +function dumpn(str) { + if (wantLogging) { + dump("NET-FRONTEND: " + str + "\n"); + } +} + +var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul index 69e2e1d3100d..9b4e1f86bdae 100644 --- a/devtools/client/netmonitor/netmonitor.xul +++ b/devtools/client/netmonitor/netmonitor.xul @@ -12,8 +12,7 @@