author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Mon, 09 Jan 2017 10:32:06 +0100 | |
changeset 328490 | 97896f92f196462e0072f8304b97c29217da1327 |
parent 328469 | d192a99be4b436f2dc839435319f7630d5d8f4b0 (current diff) |
parent 328489 | b1576b5adad1ca5a1e3ad8c6c8cfb951de19087e (diff) |
child 328517 | 701868bfddcba5bdec516be33a86dcd525dc74cf |
push id | 31174 |
push user | cbook@mozilla.com |
push date | Mon, 09 Jan 2017 09:32:17 +0000 |
treeherder | mozilla-central@97896f92f196 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 53.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/netmonitor/components/request-list-item.js +++ b/devtools/client/netmonitor/components/request-list-item.js @@ -1,21 +1,59 @@ /* 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 { createClass, createFactory, 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"); /** + * Compare two objects on a subset of their properties + */ +function propertiesEqual(props, item1, item2) { + return item1 === item2 || props.every(p => item1[p] === item2[p]); +} + +/** + * 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 UPDATED_REQ_ITEM_PROPS = [ + "mimeType", + "eventTimings", + "securityState", + "responseContentDataUri", + "status", + "statusText", + "fromCache", + "fromServiceWorker", + "method", + "url", + "remoteAddress", + "cause", + "contentSize", + "transferredSize", + "startedMillis", + "totalTime", +]; + +const UPDATED_REQ_PROPS = [ + "index", + "isSelected", + "firstRequestStartedMillis" +]; + +/** * Render one row in the request list. */ const RequestListItem = createClass({ displayName: "RequestListItem", propTypes: { item: PropTypes.object.isRequired, index: PropTypes.number.isRequired, @@ -28,20 +66,18 @@ const RequestListItem = createClass({ 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; + return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) || + !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps); }, componentDidUpdate(prevProps) { if (!prevProps.isSelected && this.props.isSelected) { this.refs.el.focus(); if (this.props.onFocusedNodeChange) { this.props.onFocusedNodeChange(); } @@ -83,218 +119,292 @@ const RequestListItem = createClass({ { 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) + 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 = [ +const UPDATED_STATUS_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]); -} +const StatusColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item); + }, -function StatusColumn(item) { - const { status, statusText, fromCache, fromServiceWorker } = item; + render() { + const { status, statusText, fromCache, fromServiceWorker } = this.props.item; - let code, title; + let code, title; - if (status) { - if (fromCache) { - code = "cached"; - } else if (fromServiceWorker) { - code = "service worker"; - } else { - code = status; - } + 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)"; + 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) - ); -} + 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) + ); + } +})); + +const MethodColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return this.props.item.method !== nextProps.item.method; + }, -function MethodColumn(item) { - const { method } = item; - return div({ className: "requests-menu-subitem requests-menu-method-box" }, - span({ className: "subitem-label requests-menu-method" }, method) - ); -} + render() { + const { method } = this.props.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; +const UPDATED_FILE_PROPS = [ + "urlDetails", + "responseContentDataUri", +]; - 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 - ) - ); -} +const FileColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { urlDetails, responseContentDataUri } = this.props.item; -function DomainColumn(item, onSecurityIconClick) { - const { urlDetails, remoteAddress, securityState } = 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 + ) + ); + } +})); + +const UPDATED_DOMAIN_PROPS = [ + "urlDetails", + "remoteAddress", + "securityState", +]; - 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}`); - } +const DomainColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { item, onSecurityIconClick } = this.props; + const { urlDetails, remoteAddress, securityState } = item; - let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : ""); + 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) - ); -} + 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; +const CauseColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return this.props.item.cause !== nextProps.item.cause; + }, - let causeType = ""; - let causeUri = undefined; - let causeHasStack = false; + render() { + const { cause } = this.props.item; + + let causeType = ""; + let causeUri = undefined; + let causeHasStack = false; - if (cause) { - causeType = cause.type; - causeUri = cause.loadingDocumentUri; - causeHasStack = cause.stacktrace && cause.stacktrace.length > 0; - } + 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) - ); -} + 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; +const TypeColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return this.props.item.mimeType !== nextProps.item.mimeType; + }, + + render() { + const { mimeType } = this.props.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) + ); } +})); - return div( - { className: "requests-menu-subitem requests-menu-type", title: mimeType }, - span({ className: "subitem-label" }, abbrevType) - ); -} +const UPDATED_TRANSFERRED_PROPS = [ + "transferredSize", + "fromCache", + "fromServiceWorker", +]; + +const TransferredSizeColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item); + }, + + render() { + const { transferredSize, fromCache, fromServiceWorker } = this.props.item; -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"); + } - 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) + ); } +})); + +const ContentSizeColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return this.props.item.contentSize !== nextProps.item.contentSize; + }, + + render() { + const { contentSize } = this.props.item; + + let text; + if (typeof contentSize == "number") { + text = getFormattedSize(contentSize); + } - return div( - { className: "requests-menu-subitem requests-menu-transferred", title: text }, - span({ className }, text) - ); -} + return div( + { + className: "requests-menu-subitem subitem-label requests-menu-size", + title: text + }, + span({ className: "subitem-label" }, text) + ); + } +})); -function ContentSizeColumn(item) { - const { contentSize } = item; +const UPDATED_WATERFALL_PROPS = [ + "eventTimings", + "totalTime", + "fromCache", + "fromServiceWorker", +]; - let text; - if (typeof contentSize == "number") { - text = getFormattedSize(contentSize); - } +const WaterfallColumn = createFactory(createClass({ + shouldComponentUpdate(nextProps) { + return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis || + !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item); + }, - return div( - { className: "requests-menu-subitem subitem-label requests-menu-size", title: text }, - span({ className: "subitem-label" }, text) - ); -} + render() { + const { item, firstRequestStartedMillis } = this.props; + 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) + ) + ); + } +})); // 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 = []; @@ -326,21 +436,9 @@ function timingBoxes(item) { 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;
new file mode 100644 --- /dev/null +++ b/devtools/client/netmonitor/shared/components/editor.js @@ -0,0 +1,92 @@ +/* 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, DOM, PropTypes } = require("devtools/client/shared/vendor/react"); +const SourceEditor = require("devtools/client/sourceeditor/editor"); + +const { div } = DOM; + +/** + * CodeMirror editor as a React component + */ +const Editor = createClass({ + displayName: "Editor", + + propTypes: { + open: PropTypes.bool, + text: PropTypes.string, + }, + + getDefaultProps() { + return { + open: true, + text: "", + }; + }, + + componentDidMount() { + const { text } = this.props; + + this.editor = new SourceEditor({ + lineNumbers: true, + readOnly: true, + value: text, + }); + + this.deferEditor = this.editor.appendTo(this.refs.editorElement); + }, + + componentDidUpdate(prevProps) { + const { mode, open, text } = this.props; + + if (!open) { + return; + } + + if (prevProps.mode !== mode) { + this.deferEditor.then(() => { + this.editor.setMode(mode); + }); + } + + if (prevProps.text !== text) { + this.deferEditor.then(() => { + // FIXME: Workaround for browser_net_accessibility test to + // make sure editor node exists while setting editor text. + // deferEditor workround should be removed in bug 1308442 + if (this.refs.editor) { + this.editor.setText(text); + } + }); + } + }, + + componentWillUnmount() { + this.deferEditor.then(() => { + this.editor.destroy(); + this.editor = null; + }); + this.deferEditor = null; + }, + + render() { + const { open } = this.props; + + return ( + div({ className: "editor-container devtools-monospace" }, + div({ + ref: "editorElement", + className: "editor-mount devtools-monospace", + // Using visibility instead of display property to avoid breaking + // CodeMirror indentation + style: { visibility: open ? "visible" : "hidden" }, + }), + ) + ); + } +}); + +module.exports = Editor;
--- a/devtools/client/netmonitor/shared/components/moz.build +++ b/devtools/client/netmonitor/shared/components/moz.build @@ -1,9 +1,11 @@ # 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( + 'editor.js', 'preview-panel.js', + 'properties-view.js', 'security-panel.js', 'timings-panel.js', )
new file mode 100644 --- /dev/null +++ b/devtools/client/netmonitor/shared/components/properties-view.js @@ -0,0 +1,162 @@ +/* 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, + createFactory, + DOM, + PropTypes, +} = require("devtools/client/shared/vendor/react"); +const { createFactories } = require("devtools/client/shared/components/reps/rep-utils"); +const { MODE } = require("devtools/client/shared/components/reps/constants"); +const { FILTER_SEARCH_DELAY } = require("../../constants"); + +// Components +const Editor = createFactory(require("devtools/client/netmonitor/shared/components/editor")); +const SearchBox = createFactory(require("devtools/client/shared/components/search-box")); +const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view")); +const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row")); +const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep")); + +const { div, tr, td } = DOM; + +/* + * Properties View component + * A scrollable tree view component which provides some useful features for + * representing object properties. + * + * Search filter - Set enableFilter to enable / disable SearchBox feature. + * Tree view - Default enabled. + * Source editor - Enable by specifying object level 1 property name to "editorText". + * Rep - Default enabled. + */ +const PropertiesView = createClass({ + displayName: "PropertiesView", + + propTypes: { + object: PropTypes.object, + enableInput: PropTypes.bool, + expandableStrings: PropTypes.bool, + filterPlaceHolder: PropTypes.string, + sectionNames: PropTypes.array, + }, + + getDefaultProps() { + return { + enableInput: true, + enableFilter: true, + expandableStrings: false, + filterPlaceHolder: "", + }; + }, + + getInitialState() { + return { + filterText: "", + }; + }, + + getRowClass(object, sectionNames) { + return sectionNames.includes(object.name) ? "tree-section" : ""; + }, + + onFilter(object, whiteList) { + let { name, value } = object; + let filterText = this.state.filterText; + + if (!filterText || whiteList.includes(name)) { + return true; + } + + let jsonString = JSON.stringify({ [name]: value }).toLowerCase(); + return jsonString.includes(filterText.toLowerCase()); + }, + + renderRowWithEditor(props) { + const { level, name, value } = props.member; + // Display source editor when prop name specify to editorText + if (level === 1 && name === "editorText") { + return ( + tr({}, + td({ colSpan: 2 }, + Editor({ text: value }) + ) + ) + ); + } + + return TreeRow(props); + }, + + renderValueWithRep(props) { + // Hide rep summary for sections + if (props.member.level === 0) { + return null; + } + + return Rep(Object.assign(props, { + // FIXME: A workaround for the issue in StringRep + // Force StringRep to crop the text everytime + member: Object.assign({}, props.member, { open: false }), + mode: MODE.TINY, + cropLimit: 60, + })); + }, + + updateFilterText(filterText) { + this.setState({ + filterText, + }); + }, + + render() { + const { + object, + decorator, + enableInput, + enableFilter, + expandableStrings, + filterPlaceHolder, + renderRow, + renderValue, + sectionNames, + } = this.props; + + return ( + div({ className: "properties-view" }, + enableFilter && div({ className: "searchbox-section" }, + SearchBox({ + delay: FILTER_SEARCH_DELAY, + type: "filter", + onChange: this.updateFilterText, + placeholder: filterPlaceHolder, + }), + ), + div({ className: "tree-container" }, + TreeView({ + object, + columns: [{ + id: "value", + width: "100%", + }], + decorator: decorator || { + getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames), + }, + enableInput, + expandableStrings, + expandedNodes: new Set(sectionNames.map((sec) => "/" + sec)), + onFilter: (props) => this.onFilter(props, sectionNames), + renderRow: renderRow || this.renderRowWithEditor, + renderValue: renderValue || this.renderValueWithRep, + }), + ), + ) + ); + } + +}); + +module.exports = PropertiesView;
--- a/devtools/client/themes/netmonitor.css +++ b/devtools/client/themes/netmonitor.css @@ -1115,34 +1115,83 @@ outline: 0; box-shadow: var(--theme-focus-box-shadow-textbox); } .treeTable .treeLabel { font-weight: 600; } -/* Customize default tree table style to align with devtools theme */ -.theme-light .treeTable .treeLabel, -.theme-light .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover { - color: var(--theme-highlight-red); +.properties-view { + /* FIXME: Minus 24px * 2 for toolbox height + panel height + * Give a fixed panel container height in order to force tree view scrollable */ + height: calc(100vh - 48px); + display: flex; + flex-direction: column; +} + +.properties-view .searchbox-section { + flex: 0 1 auto; +} + +.properties-view .devtools-searchbox { + padding: 0; +} + +.properties-view .devtools-searchbox input { + margin: 1px 3px; +} + +.tree-container { + position: relative; + height: 100%; } -.theme-dark .treeTable .treeLabel, -.theme-dark .treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover { - color: var(--theme-highlight-purple); +/* Make treeTable fill parent element and scrollable */ +.tree-container .treeTable { + position: absolute; + display: block; + overflow-y: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.properties-view .devtools-searchbox, +.tree-container .treeTable .tree-section { + width: 100%; + background-color: var(--theme-toolbar-background); +} + +.tree-container .treeTable .tree-section > * { + vertical-align: middle; } -.theme-firebug .treeTable .treeLabel { - color: var(--theme-body-color); +.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel, +.tree-container .treeTable .treeRow.tree-section > .treeLabelCell > .treeLabel:hover { + color: var(--theme-body-color-alt); +} + +.tree-container .treeTable .treeValueCell { + /* FIXME: Make value cell can be reduced to shorter width */ + max-width: 0; + padding-inline-end: 5px; } -.treeTable .treeRow.hasChildren > .treeLabelCell > .treeLabel:hover { - cursor: default; - text-decoration: none; +.tree-container .objectBox { + white-space: nowrap; +} + +.editor-container, +.editor-mount, +.editor-mount iframe { + border: none; + width: 100%; + height: 100%; } /* * FIXME: normal html block element cannot fill outer XUL element * This workaround should be removed after netmonitor is migrated to react */ #react-preview-tabpanel-hook, #react-security-tabpanel-hook,
--- a/layout/style/test/browser.ini +++ b/layout/style/test/browser.ini @@ -1,8 +1,9 @@ [DEFAULT] support-files = bug453896_iframe.html media_queries_iframe.html newtab_share_rule_processors.html [browser_bug453896.js] [browser_newtab_share_rule_processors.js] +skip-if = stylo # bug 1290224
--- a/layout/style/test/chrome/chrome.ini +++ b/layout/style/test/chrome/chrome.ini @@ -10,12 +10,14 @@ support-files = mismatch.png [test_author_specified_style.html] [test_bug418986-2.xul] [test_bug1157097.html] [test_bug1160724.xul] [test_bug535806.xul] [test_display_mode.html] +tags = fullscreen [test_display_mode_reflow.html] tags = fullscreen [test_hover.html] [test_moz_document_rules.html] +skip-if = stylo # bug 1290224
--- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -31,19 +31,23 @@ support-files = visited_image_loading_frame.html visited_image_loading.sjs visited-lying-inner.html visited-pref-iframe.html xbl_bindings.xml [test_acid3_test46.html] [test_addSheet.html] +skip-if = stylo # bug 1290224 support-files = additional_sheets_helper.html [test_additional_sheets.html] +skip-if = stylo # bug 1290224 support-files = additional_sheets_helper.html +[test_align_justify_computed_values.html] +[test_align_shorthand_serialization.html] [test_all_shorthand.html] [test_animations.html] skip-if = toolkit == 'android' [test_animations_async_tests.html] support-files = ../../reftests/fonts/Ahem.ttf file_animations_async_tests.html [test_animations_dynamic_changes.html] [test_animations_effect_timing_duration.html] support-files = file_animations_effect_timing_duration.html @@ -57,20 +61,23 @@ support-files = file_animations_effect_t support-files = file_animations_iterationstart.html [test_animations_omta.html] [test_animations_omta_start.html] [test_animations_pausing.html] support-files = file_animations_pausing.html [test_animations_playbackrate.html] support-files = file_animations_playbackrate.html [test_animations_styles_on_event.html] +skip-if = stylo # timeout bug 1328505 support-files = file_animations_styles_on_event.html [test_animations_with_disabled_properties.html] +skip-if = stylo # timeout bug 1328503 support-files = file_animations_with_disabled_properties.html [test_any_dynamic.html] +[test_asyncopen2.html] [test_at_rule_parse_serialize.html] [test_attribute_selector_eof_behavior.html] [test_background_blend_mode.html] [test_box_size_keywords.html] [test_bug73586.html] [test_bug74880.html] [test_bug98997.html] [test_bug160403.html] @@ -137,16 +144,17 @@ support-files = bug732209-css.sjs [test_bug829816.html] [test_bug874919.html] support-files = file_bug829816.css [test_bug887741_at-rules_in_declaration_lists.html] [test_bug892929.html] [test_bug1055933.html] support-files = file_bug1055933_circle-xxl.png [test_bug1089417.html] +skip-if = stylo # bug 1323665 support-files = file_bug1089417_iframe.html [test_bug1112014.html] [test_bug1203766.html] [test_bug1232829.html] [test_bug1292447.html] [test_cascade.html] [test_ch_ex_no_infloops.html] [test_change_hint_optimizations.html] @@ -164,26 +172,27 @@ skip-if = toolkit == 'android' [test_counter_style.html] [test_css_cross_domain.html] skip-if = toolkit == 'android' #bug 536603 [test_css_eof_handling.html] [test_css_escape_api.html] [test_css_function_mismatched_parenthesis.html] [test_css_loader_crossorigin_data_url.html] [test_css_supports.html] +skip-if = stylo # bug 1323715 [test_css_supports_variables.html] +skip-if = stylo # bug 1323715 [test_default_bidi_css.html] [test_default_computed_style.html] [test_descriptor_storage.html] [test_descriptor_syntax_errors.html] [test_dont_use_document_colors.html] [test_dynamic_change_causing_reflow.html] [test_exposed_prop_accessors.html] [test_extra_inherit_initial.html] -[test_align_justify_computed_values.html] [test_flexbox_child_display_values.xhtml] [test_flexbox_flex_grow_and_shrink.html] [test_flexbox_flex_shorthand.html] [test_flexbox_layout.html] support-files = flexbox_layout_testcases.js [test_flexbox_order.html] [test_flexbox_order_table.html] [test_flexbox_reflow_counts.html] @@ -201,21 +210,19 @@ support-files = [test_grid_computed_values.html] [test_group_insertRule.html] [test_hover_quirk.html] [test_html_attribute_computed_values.html] [test_ident_escaping.html] [test_inherit_computation.html] skip-if = toolkit == 'android' [test_inherit_storage.html] -tags = stylo [test_initial_computation.html] skip-if = toolkit == 'android' [test_initial_storage.html] -tags = stylo [test_keyframes_rules.html] [test_load_events_on_stylesheets.html] [test_logical_properties.html] [test_media_queries.html] skip-if = android_version == '18' #debug-only failure; timed out #Android 4.3 aws only; bug 1030419 [test_media_queries_dynamic.html] [test_media_queries_dynamic_xbl.html] [test_media_query_list.html] @@ -233,16 +240,17 @@ skip-if = android_version == '18' #debug [test_position_float_display.html] [test_position_sticky.html] [test_priority_preservation.html] [test_property_database.html] [test_property_syntax_errors.html] [test_pseudoelement_state.html] [test_pseudoelement_parsing.html] [test_redundant_font_download.html] +skip-if = stylo # bug 1323665 support-files = redundant_font_download.sjs [test_rem_unit.html] [test_restyles_in_smil_animation.html] [test_root_node_display.html] [test_rule_insertion.html] [test_rule_serialization.html] [test_rules_out_of_sheets.html] [test_selectors.html] @@ -255,60 +263,64 @@ skip-if = toolkit == 'android' #bug 7752 [test_style_attribute_standards.html] [test_style_struct_copy_constructors.html] [test_supports_rules.html] [test_system_font_serialization.html] [test_text_decoration_shorthands.html] [test_transitions_and_reframes.html] [test_transitions_and_restyles.html] [test_transitions_and_zoom.html] +skip-if = stylo # timeout bug 1328499 [test_transitions_cancel_near_end.html] +skip-if = stylo # timeout bug 1328499 [test_transitions_computed_values.html] [test_transitions_computed_value_combinations.html] [test_transitions_events.html] +skip-if = stylo # timeout bug 1328499 [test_transitions.html] skip-if = (android_version == '18' && debug) # bug 1159532 [test_transitions_bug537151.html] +skip-if = stylo # timeout bug 1328499 [test_transitions_dynamic_changes.html] [test_transitions_per_property.html] -skip-if = toolkit == 'android' #bug 775227 +skip-if = (toolkit == 'android' || stylo) # bug 775227 for android, bug 1292283 for stylo [test_transitions_replacement_on_busy_frame.html] +skip-if = stylo # timeout bug 1328503 support-files = file_transitions_replacement_on_busy_frame.html [test_transitions_step_functions.html] [test_transitions_with_disabled_properties.html] support-files = file_transitions_with_disabled_properties.html [test_unclosed_parentheses.html] [test_unicode_range_loading.html] +skip-if = stylo # timeout bug 1328507 support-files = ../../reftests/fonts/markA.woff ../../reftests/fonts/markB.woff ../../reftests/fonts/markC.woff ../../reftests/fonts/markD.woff [test_units_angle.html] [test_units_frequency.html] [test_units_length.html] [test_units_time.html] [test_unprefixing_service.html] support-files = unprefixing_service_iframe.html unprefixing_service_utils.js [test_unprefixing_service_prefs.html] support-files = unprefixing_service_iframe.html unprefixing_service_utils.js [test_value_cloning.html] -tags = stylo skip-if = toolkit == 'android' #bug 775227 [test_value_computation.html] skip-if = toolkit == 'android' [test_value_storage.html] +skip-if = stylo # bug 1329533 [test_variable_serialization_computed.html] [test_variable_serialization_specified.html] [test_variables.html] support-files = support/external-variable-url.css [test_video_object_fit.html] [test_viewport_units.html] [test_visited_image_loading.html] -skip-if = toolkit == 'android' #TIMED_OUT +skip-if = (toolkit == 'android' || stylo) # TIMED_OUT for android, timeout bug 1328511 for stylo [test_visited_image_loading_empty.html] -skip-if = toolkit == 'android' #TIMED_OUT +skip-if = (toolkit == 'android' || stylo) # TIMED_OUT for android, timeout bug 1328511 for stylo [test_visited_lying.html] -skip-if = toolkit == 'android' #TIMED_OUT +skip-if = (toolkit == 'android' || stylo) # TIMED_OUT for android, timeout bug 1328511 for stylo [test_visited_pref.html] -skip-if = toolkit == 'android' #TIMED_OUT +skip-if = (toolkit == 'android' || stylo) # TIMED_OUT for android, timeout bug 1328511 for stylo [test_visited_reftests.html] -skip-if = toolkit == 'android' #TIMED_OUT +skip-if = (toolkit == 'android' || stylo) # TIMED_OUT for android, timeout bug 1328511 for stylo [test_webkit_device_pixel_ratio.html] [test_webkit_flex_display.html] -[test_asyncopen2.html] -[test_align_shorthand_serialization.html]
--- a/services/sync/modules/engines/clients.js +++ b/services/sync/modules/engines/clients.js @@ -246,20 +246,21 @@ ClientEngine.prototype = { }) ); }, _addClientCommand(clientId, command) { const allCommands = this._readCommands(); const clientCommands = allCommands[clientId] || []; if (hasDupeCommand(clientCommands, command)) { - return; + return false; } allCommands[clientId] = clientCommands.concat(command); this._saveCommands(allCommands); + return true; }, _syncStartup: function _syncStartup() { // Reupload new client record periodically. if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) { this._tracker.addChangedID(this.localID); this.lastRecordUpload = Date.now() / 1000; } @@ -472,35 +473,45 @@ ClientEngine.prototype = { /** * Sends a command+args pair to a specific client. * * @param command Command string * @param args Array of arguments/data for command * @param clientId Client to send command to */ - _sendCommandToClient: function sendCommandToClient(command, args, clientId) { + _sendCommandToClient: function sendCommandToClient(command, args, clientId, flowID = null) { this._log.trace("Sending " + command + " to " + clientId); let client = this._store._remoteClients[clientId]; if (!client) { throw new Error("Unknown remote client ID: '" + clientId + "'."); } if (client.stale) { throw new Error("Stale remote client ID: '" + clientId + "'."); } let action = { command: command, args: args, + flowID: flowID || Utils.makeGUID(), // used for telemetry. }; - this._log.trace("Client " + clientId + " got a new action: " + [command, args]); - this._addClientCommand(clientId, action); - this._tracker.addChangedID(clientId); + if (this._addClientCommand(clientId, action)) { + this._log.trace(`Client ${clientId} got a new action`, [command, args]); + this._tracker.addChangedID(clientId); + let deviceID; + try { + deviceID = this.service.identity.hashedDeviceID(clientId); + } catch (_) {} + this.service.recordTelemetryEvent("sendcommand", command, undefined, + { flowID: action.flowID, deviceID }); + } else { + this._log.trace(`Client ${clientId} got a duplicate action`, [command, args]); + } }, /** * Check if the local client has any remote commands and perform them. * * @return false to abort sync */ processIncomingCommands: function processIncomingCommands() { @@ -510,19 +521,22 @@ ClientEngine.prototype = { } const clearedCommands = this._readCommands()[this.localID]; const commands = this.localCommands.filter(command => !hasDupeCommand(clearedCommands, command)); let URIsToDisplay = []; // Process each command in order. for (let rawCommand of commands) { - let {command, args} = rawCommand; + let {command, args, flowID} = rawCommand; this._log.debug("Processing command: " + command + "(" + args + ")"); + this.service.recordTelemetryEvent("processcommand", command, undefined, + { flowID }); + let engines = [args[0]]; switch (command) { case "resetAll": engines = null; // Fallthrough case "resetEngine": this.service.resetClient(engines); break; @@ -565,37 +579,40 @@ ClientEngine.prototype = { * * @param command * Command to invoke on remote clients * @param args * Array of arguments to give to the command * @param clientId * Client ID to send command to. If undefined, send to all remote * clients. + * @param flowID + * A unique identifier used to track success for this operation across + * devices. */ - sendCommand: function sendCommand(command, args, clientId) { + sendCommand: function sendCommand(command, args, clientId, flowID = null) { let commandData = this._commands[command]; // Don't send commands that we don't know about. if (!commandData) { this._log.error("Unknown command to send: " + command); return; } // Don't send a command with the wrong number of arguments. else if (!args || args.length != commandData.args) { this._log.error("Expected " + commandData.args + " args for '" + command + "', but got " + args); return; } if (clientId) { - this._sendCommandToClient(command, args, clientId); + this._sendCommandToClient(command, args, clientId, flowID); } else { for (let [id, record] of Object.entries(this._store._remoteClients)) { if (!record.stale) { - this._sendCommandToClient(command, args, id); + this._sendCommandToClient(command, args, id, flowID); } } } }, /** * Send a URI to another client for display. *
--- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -1745,12 +1745,16 @@ Sync11Service.prototype = { this._log.debug("Server returned invalid JSON for '" + info_type + "': " + this.response.body); return callback(ex); } this._log.trace("Successfully retrieved '" + info_type + "'."); return callback(null, result); }); }, + + recordTelemetryEvent(object, method, value, extra = undefined) { + Svc.Obs.notify("weave:telemetry:event", { object, method, value, extra }); + }, }; this.Service = new Sync11Service(); Service.onStartup();
--- a/services/sync/modules/telemetry.js +++ b/services/sync/modules/telemetry.js @@ -40,16 +40,18 @@ const TOPICS = [ "weave:engine:sync:start", "weave:engine:sync:finish", "weave:engine:sync:error", "weave:engine:sync:applied", "weave:engine:sync:uploaded", "weave:engine:validate:finish", "weave:engine:validate:error", + + "weave:telemetry:event", ]; const PING_FORMAT_VERSION = 1; const EMPTY_UID = "0".repeat(32); // The set of engines we record telemetry for - any other engines are ignored. const ENGINES = new Set(["addons", "bookmarks", "clients", "forms", "history", @@ -122,16 +124,54 @@ function tryGetMonotonicTimestamp() { function timeDeltaFrom(monotonicStartTime) { let now = tryGetMonotonicTimestamp(); if (monotonicStartTime !== -1 && now !== -1) { return Math.round(now - monotonicStartTime); } return -1; } +// This function validates the payload of a telemetry "event" - this can be +// removed once there are APIs available for the telemetry modules to collect +// these events (bug 1329530) - but for now we simulate that planned API as +// best we can. +function validateTelemetryEvent(eventDetails) { + let { object, method, value, extra } = eventDetails; + // Do do basic validation of the params - everything except "extra" must + // be a string. method and object are required. + if (typeof method != "string" || typeof object != "string" || + (value && typeof value != "string") || + (extra && typeof extra != "object")) { + log.warn("Invalid event parameters - wrong types", eventDetails); + return false; + } + // length checks. + if (method.length > 20 || object.length > 20 || + (value && value.length > 80)) { + log.warn("Invalid event parameters - wrong lengths", eventDetails); + return false; + } + + // extra can be falsey, or an object with string names and values. + if (extra) { + if (Object.keys(extra).length > 10) { + log.warn("Invalid event parameters - too many extra keys", eventDetails); + return false; + } + for (let [ename, evalue] of Object.entries(extra)) { + if (typeof ename != "string" || ename.length > 15 || + typeof evalue != "string" || evalue.length > 85) { + log.warn(`Invalid event parameters: extra item "${ename} is invalid`, eventDetails); + return false; + } + } + } + return true; +} + class EngineRecord { constructor(name) { // startTime is in ms from process start, but is monotonic (unlike Date.now()) // so we need to keep both it and when. this.startTime = tryGetMonotonicTimestamp(); this.name = name; } @@ -411,40 +451,44 @@ class SyncTelemetryImpl { log.level = Log.Level[Svc.Prefs.get("log.logger.telemetry", "Trace")]; // This is accessible so we can enable custom engines during tests. this.allowedEngines = allowedEngines; this.current = null; this.setupObservers(); this.payloads = []; this.discarded = 0; + this.events = []; + this.maxEventsCount = Svc.Prefs.get("telemetry.maxEventsCount", 1000); this.maxPayloadCount = Svc.Prefs.get("telemetry.maxPayloadCount"); this.submissionInterval = Svc.Prefs.get("telemetry.submissionInterval") * 1000; this.lastSubmissionTime = Telemetry.msSinceProcessStart(); this.lastUID = EMPTY_UID; this.lastDeviceID = undefined; } getPingJSON(reason) { return { why: reason, discarded: this.discarded || undefined, version: PING_FORMAT_VERSION, syncs: this.payloads.slice(), uid: this.lastUID, deviceID: this.lastDeviceID, + events: this.events.length == 0 ? undefined : this.events, }; } finish(reason) { // Note that we might be in the middle of a sync right now, and so we don't // want to touch this.current. let result = this.getPingJSON(reason); this.payloads = []; this.discarded = 0; + this.events = []; this.submit(result); } setupObservers() { for (let topic of TOPICS) { Observers.add(topic, this, this); } } @@ -525,16 +569,49 @@ class SyncTelemetryImpl { } this.current = null; if ((Telemetry.msSinceProcessStart() - this.lastSubmissionTime) > this.submissionInterval) { this.finish("schedule"); this.lastSubmissionTime = Telemetry.msSinceProcessStart(); } } + _recordEvent(eventDetails) { + if (this.events.length >= this.maxEventsCount) { + log.warn("discarding event - already queued our maximum", eventDetails); + return; + } + + if (!validateTelemetryEvent(eventDetails)) { + // we've already logged what the problem is... + return; + } + log.debug("recording event", eventDetails); + + let { object, method, value, extra } = eventDetails; + let category = "sync"; + let ts = Math.floor(tryGetMonotonicTimestamp()); + + // An event record is a simple array with at least 4 items. + let event = [ts, category, method, object]; + // It may have up to 6 elements if |extra| is defined + if (value) { + event.push(value); + if (extra) { + event.push(extra); + } + } else { + if (extra) { + event.push(null); // a null for the empty value. + event.push(extra); + } + } + this.events.push(event); + } + observe(subject, topic, data) { log.trace(`observed ${topic} ${data}`); switch (topic) { case "profile-before-change": this.shutdown(); break; @@ -593,16 +670,20 @@ class SyncTelemetryImpl { break; case "weave:engine:validate:error": if (this._checkCurrent(topic)) { this.current.onEngineValidateError(data, subject || "Unknown"); } break; + case "weave:telemetry:event": + this._recordEvent(subject); + break; + default: log.warn(`unexpected observer topic ${topic}`); break; } } } this.SyncTelemetry = new SyncTelemetryImpl(ENGINES);
--- a/services/sync/tests/unit/head_helpers.js +++ b/services/sync/tests/unit/head_helpers.js @@ -271,17 +271,25 @@ function get_sync_test_telemetry() { function assert_valid_ping(record) { // This is called as the test harness tears down due to shutdown. This // will typically have no recorded syncs, and the validator complains about // it. So ignore such records (but only ignore when *both* shutdown and // no Syncs - either of them not being true might be an actual problem) if (record && (record.why != "shutdown" || record.syncs.length != 0)) { if (!SyncPingValidator(record)) { - deepEqual([], SyncPingValidator.errors, "Sync telemetry ping validation failed"); + if (SyncPingValidator.errors.length) { + // validation failed - using a simple |deepEqual([], errors)| tends to + // truncate the validation errors in the output and doesn't show that + // the ping actually was - so be helpful. + do_print("telemetry ping validation failed"); + do_print("the ping data is: " + JSON.stringify(record, undefined, 2)); + do_print("the validation failures: " + JSON.stringify(SyncPingValidator.errors, undefined, 2)); + ok(false, "Sync telemetry ping validation failed - see output above for details"); + } } equal(record.version, 1); record.syncs.forEach(p => { lessOrEqual(p.when, Date.now()); if (p.devices) { ok(!p.devices.some(device => device.id == record.deviceID)); equal(new Set(p.devices.map(device => device.id)).size, p.devices.length, "Duplicate device ids in ping devices list");
--- a/services/sync/tests/unit/sync_ping_schema.json +++ b/services/sync/tests/unit/sync_ping_schema.json @@ -15,16 +15,21 @@ "deviceID": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "syncs": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/payload" } + }, + "events": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/event" } } }, "definitions": { "payload": { "type": "object", "additionalProperties": false, "required": ["when", "took"], "properties": { @@ -123,16 +128,21 @@ {"required": ["sent"]}, {"required": ["failed"]} ], "properties": { "sent": { "type": "integer", "minimum": 1 }, "failed": { "type": "integer", "minimum": 1 } } }, + "event": { + "type": "array", + "minItems": 4, + "maxItems": 6 + }, "error": { "oneOf": [ { "$ref": "#/definitions/httpError" }, { "$ref": "#/definitions/nsError" }, { "$ref": "#/definitions/shutdownError" }, { "$ref": "#/definitions/authError" }, { "$ref": "#/definitions/otherError" }, { "$ref": "#/definitions/unexpectedError" },
--- a/services/sync/tests/unit/test_clients_engine.js +++ b/services/sync/tests/unit/test_clients_engine.js @@ -384,16 +384,17 @@ add_test(function test_send_command() { let clientCommands = engine._readCommands()[remoteId]; notEqual(newRecord, undefined); equal(clientCommands.length, 1); let command = clientCommands[0]; equal(command.command, action); equal(command.args.length, 2); deepEqual(command.args, args); + ok(command.flowID); notEqual(tracker.changedIDs[remoteId], undefined); engine._tracker.clearChangedIDs(); run_next_test(); }); add_test(function test_command_validation() { @@ -627,22 +628,22 @@ add_task(async function test_filter_dupl equal(counts.newFailed, 0); _("Broadcast logout to all clients"); engine.sendCommand("logout", []); engine._sync(); let collection = server.getCollection("foo", "clients"); let recentPayload = JSON.parse(JSON.parse(collection.payload(recentID)).ciphertext); - deepEqual(recentPayload.commands, [{ command: "logout", args: [] }], - "Should send commands to the recent client"); + compareCommands(recentPayload.commands, [{ command: "logout", args: [] }], + "Should send commands to the recent client"); let oldPayload = JSON.parse(JSON.parse(collection.payload(oldID)).ciphertext); - deepEqual(oldPayload.commands, [{ command: "logout", args: [] }], - "Should send commands to the week-old client"); + compareCommands(oldPayload.commands, [{ command: "logout", args: [] }], + "Should send commands to the week-old client"); let dupePayload = JSON.parse(JSON.parse(collection.payload(dupeID)).ciphertext); deepEqual(dupePayload.commands, [], "Should not send commands to the dupe client"); _("Update the dupe client's modified time"); server.insertWBO("foo", "clients", new ServerWBO(dupeID, encryptPayload({ id: dupeID, @@ -909,29 +910,31 @@ add_task(async function test_merge_comma let desktopID = Utils.makeGUID(); server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({ id: desktopID, name: "Desktop client", type: "desktop", commands: [{ command: "displayURI", args: ["https://example.com", engine.localID, "Yak Herders Anonymous"], + flowID: Utils.makeGUID(), }], version: "48", protocols: ["1.5"], }), now - 10)); let mobileID = Utils.makeGUID(); server.insertWBO("foo", "clients", new ServerWBO(mobileID, encryptPayload({ id: mobileID, name: "Mobile client", type: "mobile", commands: [{ command: "logout", args: [], + flowID: Utils.makeGUID(), }], version: "48", protocols: ["1.5"], }), now - 10)); try { let store = engine._store; @@ -940,27 +943,27 @@ add_task(async function test_merge_comma engine._sync(); _("Broadcast logout to all clients"); engine.sendCommand("logout", []); engine._sync(); let collection = server.getCollection("foo", "clients"); let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext); - deepEqual(desktopPayload.commands, [{ + compareCommands(desktopPayload.commands, [{ command: "displayURI", args: ["https://example.com", engine.localID, "Yak Herders Anonymous"], }, { command: "logout", args: [], }], "Should send the logout command to the desktop client"); let mobilePayload = JSON.parse(JSON.parse(collection.payload(mobileID)).ciphertext); - deepEqual(mobilePayload.commands, [{ command: "logout", args: [] }], - "Should not send a duplicate logout to the mobile client"); + compareCommands(mobilePayload.commands, [{ command: "logout", args: [] }], + "Should not send a duplicate logout to the mobile client"); } finally { Svc.Prefs.resetBranch(""); Service.recordManager.clearCache(); engine._resetClient(); try { server.deleteCollections("foo"); } finally { @@ -1017,17 +1020,17 @@ add_task(async function test_duplicate_r }), now - 10)); _("Send another tab to the desktop client"); engine.sendCommand("displayURI", ["https://foobar.com", engine.localID, "Foo bar!"], desktopID); engine._sync(); let collection = server.getCollection("foo", "clients"); let desktopPayload = JSON.parse(JSON.parse(collection.payload(desktopID)).ciphertext); - deepEqual(desktopPayload.commands, [{ + compareCommands(desktopPayload.commands, [{ command: "displayURI", args: ["https://foobar.com", engine.localID, "Foo bar!"], }], "Should only send the second command to the desktop client"); } finally { Svc.Prefs.resetBranch(""); Service.recordManager.clearCache(); engine._resetClient(); @@ -1057,17 +1060,19 @@ add_task(async function test_upload_afte let deviceBID = Utils.makeGUID(); let deviceCID = Utils.makeGUID(); server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({ id: deviceBID, name: "Device B", type: "desktop", commands: [{ - command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"] + command: "displayURI", + args: ["https://deviceclink.com", deviceCID, "Device C link"], + flowID: Utils.makeGUID(), }], version: "48", protocols: ["1.5"], }), now - 10)); server.insertWBO("foo", "clients", new ServerWBO(deviceCID, encryptPayload({ id: deviceCID, name: "Device C", type: "desktop", @@ -1087,17 +1092,17 @@ add_task(async function test_upload_afte engine.sendCommand("displayURI", ["https://example.com", engine.localID, "Yak Herders Anonymous"], deviceBID); const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing; SyncEngine.prototype._uploadOutgoing = () => engine._onRecordsWritten.call(engine, [], [deviceBID]); engine._sync(); let collection = server.getCollection("foo", "clients"); let deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext); - deepEqual(deviceBPayload.commands, [{ + compareCommands(deviceBPayload.commands, [{ command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"] }], "Should be the same because the upload failed"); _("Simulate the client B consuming the command and syncing to the server"); server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({ id: deviceBID, name: "Device B", type: "desktop", @@ -1108,17 +1113,17 @@ add_task(async function test_upload_afte // Simulate reboot SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing; engine = Service.clientsEngine = new ClientEngine(Service); engine._sync(); deviceBPayload = JSON.parse(JSON.parse(collection.payload(deviceBID)).ciphertext); - deepEqual(deviceBPayload.commands, [{ + compareCommands(deviceBPayload.commands, [{ command: "displayURI", args: ["https://example.com", engine.localID, "Yak Herders Anonymous"], }], "Should only had written our outgoing command"); } finally { Svc.Prefs.resetBranch(""); Service.recordManager.clearCache(); engine._resetClient(); @@ -1148,20 +1153,24 @@ add_task(async function test_keep_cleare let deviceBID = Utils.makeGUID(); let deviceCID = Utils.makeGUID(); server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({ id: engine.localID, name: "Device A", type: "desktop", commands: [{ - command: "displayURI", args: ["https://deviceblink.com", deviceBID, "Device B link"] + command: "displayURI", + args: ["https://deviceblink.com", deviceBID, "Device B link"], + flowID: Utils.makeGUID(), }, { - command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"] + command: "displayURI", + args: ["https://deviceclink.com", deviceCID, "Device C link"], + flowID: Utils.makeGUID(), }], version: "48", protocols: ["1.5"], }), now - 10)); server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({ id: deviceBID, name: "Device B", type: "desktop", @@ -1190,36 +1199,42 @@ add_task(async function test_keep_cleare let commandsProcessed = 0; engine._handleDisplayURIs = (uris) => { commandsProcessed = uris.length }; engine._sync(); engine.processIncomingCommands(); // Not called by the engine.sync(), gotta call it ourselves equal(commandsProcessed, 2, "We processed 2 commands"); let localRemoteRecord = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext); - deepEqual(localRemoteRecord.commands, [{ + compareCommands(localRemoteRecord.commands, [{ command: "displayURI", args: ["https://deviceblink.com", deviceBID, "Device B link"] }, { command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"] }], "Should be the same because the upload failed"); // Another client sends another link server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({ id: engine.localID, name: "Device A", type: "desktop", commands: [{ - command: "displayURI", args: ["https://deviceblink.com", deviceBID, "Device B link"] + command: "displayURI", + args: ["https://deviceblink.com", deviceBID, "Device B link"], + flowID: Utils.makeGUID(), }, { - command: "displayURI", args: ["https://deviceclink.com", deviceCID, "Device C link"] + command: "displayURI", + args: ["https://deviceclink.com", deviceCID, "Device C link"], + flowID: Utils.makeGUID(), }, { - command: "displayURI", args: ["https://deviceclink2.com", deviceCID, "Device C link 2"] + command: "displayURI", + args: ["https://deviceclink2.com", deviceCID, "Device C link 2"], + flowID: Utils.makeGUID(), }], version: "48", protocols: ["1.5"], }), now - 10)); // Simulate reboot SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing; engine = Service.clientsEngine = new ClientEngine(Service); @@ -1297,17 +1312,17 @@ add_task(async function test_deleted_com _("Broadcast a command to all clients"); engine.sendCommand("logout", []); engine._sync(); deepEqual(collection.keys().sort(), [activeID, engine.localID].sort(), "Should not reupload deleted clients"); let activePayload = JSON.parse(JSON.parse(collection.payload(activeID)).ciphertext); - deepEqual(activePayload.commands, [{ command: "logout", args: [] }], + compareCommands(activePayload.commands, [{ command: "logout", args: [] }], "Should send the command to the active client"); } finally { Svc.Prefs.resetBranch(""); Service.recordManager.clearCache(); engine._resetClient(); try { server.deleteCollections("foo"); @@ -1341,28 +1356,29 @@ add_task(async function test_send_uri_ac let collection = server.getCollection("foo", "clients"); let ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext); ok(ourPayload, "Should upload our client record"); _("Send a URL to the device on the server"); ourPayload.commands = [{ command: "displayURI", args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"], + flowID: Utils.makeGUID(), }]; server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload(ourPayload), now)); _("Sync again"); engine._sync(); - deepEqual(engine.localCommands, [{ + compareCommands(engine.localCommands, [{ command: "displayURI", args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"], }], "Should receive incoming URI"); ok(engine.processIncomingCommands(), "Should process incoming commands"); const clearedCommands = engine._readCommands()[engine.localID]; - deepEqual(clearedCommands, [{ + compareCommands(clearedCommands, [{ command: "displayURI", args: ["https://example.com", fakeSenderID, "Yak Herders Anonymous"], }], "Should mark the commands as cleared after processing"); _("Check that the command was removed on the server"); engine._sync(); ourPayload = JSON.parse(JSON.parse(collection.payload(engine.localID)).ciphertext); ok(ourPayload, "Should upload the synced client record");
--- a/services/sync/tests/unit/test_telemetry.js +++ b/services/sync/tests/unit/test_telemetry.js @@ -560,9 +560,97 @@ add_task(async function test_no_foreign_ await SyncTestingInfrastructure(server); try { let ping = await sync_and_validate_telem(); ok(ping.engines.every(e => e.name !== "bogus")); } finally { Service.engineManager.unregister(engine); await cleanAndGo(engine, server); } -}); \ No newline at end of file +}); + +add_task(async function test_events() { + Service.engineManager.register(BogusEngine); + let engine = Service.engineManager.get("bogus"); + engine.enabled = true; + let server = serverForUsers({"foo": "password"}, { + meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}}, + steam: {} + }); + + await SyncTestingInfrastructure(server); + try { + Service.recordTelemetryEvent("object", "method", "value", { foo: "bar" }); + let ping = await wait_for_ping(() => Service.sync(), true, true); + equal(ping.events.length, 1); + let [timestamp, category, method, object, value, extra] = ping.events[0]; + ok((typeof timestamp == "number") && timestamp > 0); // timestamp. + equal(category, "sync"); + equal(method, "method"); + equal(object, "object"); + equal(value, "value"); + deepEqual(extra, { foo: "bar" }); + // Test with optional values. + Service.recordTelemetryEvent("object", "method"); + ping = await wait_for_ping(() => Service.sync(), false, true); + equal(ping.events.length, 1); + equal(ping.events[0].length, 4); + + Service.recordTelemetryEvent("object", "method", "extra"); + ping = await wait_for_ping(() => Service.sync(), false, true); + equal(ping.events.length, 1); + equal(ping.events[0].length, 5); + + Service.recordTelemetryEvent("object", "method", undefined, { foo: "bar" }); + ping = await wait_for_ping(() => Service.sync(), false, true); + equal(ping.events.length, 1); + equal(ping.events[0].length, 6); + [timestamp, category, method, object, value, extra] = ping.events[0]; + equal(value, null); + } finally { + Service.engineManager.unregister(engine); + await cleanAndGo(engine, server); + } +}); + +add_task(async function test_invalid_events() { + Service.engineManager.register(BogusEngine); + let engine = Service.engineManager.get("bogus"); + engine.enabled = true; + let server = serverForUsers({"foo": "password"}, { + meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}}, + steam: {} + }); + + async function checkNotRecorded(...args) { + Service.recordTelemetryEvent.call(args); + let ping = await wait_for_ping(() => Service.sync(), false, true); + equal(ping.events, undefined); + } + + await SyncTestingInfrastructure(server); + try { + let long21 = "l".repeat(21); + let long81 = "l".repeat(81); + let long86 = "l".repeat(86); + await checkNotRecorded("object"); + await checkNotRecorded("object", 2); + await checkNotRecorded(2, "method"); + await checkNotRecorded("object", "method", 2); + await checkNotRecorded("object", "method", "value", 2); + await checkNotRecorded("object", "method", "value", { foo: 2 }); + await checkNotRecorded(long21, "method", "value"); + await checkNotRecorded("object", long21, "value"); + await checkNotRecorded("object", "method", long81); + let badextra = {}; + badextra[long21] = "x"; + await checkNotRecorded("object", "method", "value", badextra); + badextra = { "x": long86 }; + await checkNotRecorded("object", "method", "value", badextra); + for (let i = 0; i < 10; i++) { + badextra["name" + i] = "x"; + } + await checkNotRecorded("object", "method", "value", badextra); + } finally { + Service.engineManager.unregister(engine); + await cleanAndGo(engine, server); + } +});
--- a/taskcluster/ci/test/test-sets.yml +++ b/taskcluster/ci/test/test-sets.yml @@ -62,17 +62,17 @@ talos: ## # Limited test sets for specific platforms stylo-tests: - cppunit - crashtest - reftest-stylo - - mochitest-stylo + - mochitest-style ccov-code-coverage-tests: - mochitest - mochitest-browser-chrome - mochitest-devtools-chrome - xpcshell jsdcov-code-coverage-tests:
--- a/taskcluster/ci/test/tests.yml +++ b/taskcluster/ci/test/tests.yml @@ -765,32 +765,32 @@ mochitest-webgl: - remove_executables.py - unittests/mac_unittest.py linux.*: - unittests/linux_unittest.py - remove_executables.py extra-options: - --mochitest-suite=mochitest-gl -mochitest-stylo: - description: "Mochitest run for Stylo" - suite: mochitest/mochitest-stylo +mochitest-style: + description: "Mochitest plain run for style system" + suite: mochitest/mochitest-style treeherder-symbol: tc-M(s) loopback-video: true e10s: false mozharness: script: desktop_unittest.py no-read-buildbot-config: true config: by-test-platform: default: - unittests/linux_unittest.py - remove_executables.py extra-options: - - --mochitest-suite=mochitest-stylo + - --mochitest-suite=mochitest-style reftest: description: "Reftest run" suite: reftest/reftest treeherder-symbol: tc-R(R) docker-image: by-test-platform: android.*: {'in-tree': 'desktop-test'}
--- a/testing/mozharness/configs/unittests/linux_unittest.py +++ b/testing/mozharness/configs/unittests/linux_unittest.py @@ -198,17 +198,17 @@ config = { "mochitest-gl": ["--subsuite=webgl"], "mochitest-devtools-chrome": ["--flavor=browser", "--subsuite=devtools"], "mochitest-devtools-chrome-chunked": ["--flavor=browser", "--subsuite=devtools", "--chunk-by-runtime"], "mochitest-devtools-chrome-coverage": ["--flavor=browser", "--subsuite=devtools", "--chunk-by-runtime", "--timeout=1200"], "jetpack-package": ["--flavor=jetpack-package"], "jetpack-package-clipboard": ["--flavor=jetpack-package", "--subsuite=clipboard"], "jetpack-addon": ["--flavor=jetpack-addon"], "a11y": ["--flavor=a11y"], - "mochitest-stylo": ["--disable-e10s", "--tag=stylo"], + "mochitest-style": ["--disable-e10s", "layout/style/test"], }, # local reftest suites "all_reftest_suites": { "crashtest": { "options": ["--suite=crashtest"], "tests": ["tests/reftest/tests/testing/crashtest/crashtests.list"] }, "jsreftest": {
--- a/toolkit/components/telemetry/docs/data/sync-ping.rst +++ b/toolkit/components/telemetry/docs/data/sync-ping.rst @@ -92,17 +92,20 @@ Structure: } ], // Format is same as above, this is only included if we tried and failed // to run validation, and if it's present, all other fields in this object are optional. failureReason: { ... }, } } ] - }] + }], + events: [ + event_array // See events below, + ] } } info ---- discarded ~~~~~~~~~ @@ -175,8 +178,47 @@ syncs.engine.validation.problems ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For engines that can run validation on themselves, an array of objects describing validation errors that have occurred. Items that would have a count of 0 are excluded. Each engine will have its own set of items that it might put in the ``name`` field, but there are a finite number. See ``BookmarkProblemData.getSummary`` in `services/sync/modules/bookmark\_validator.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/bookmark_validator.js>`_ for an example. syncs.devices ~~~~~~~~~~~~~ The list of remote devices associated with this account, as reported by the clients collection. The ID of each device is hashed using the same algorithm as the local id. + + +Events in the "sync" ping +========================= + +The sync ping includes events in the same format as they are included in the +main ping. The documentation for these events will land in bug 1302666. + +Every event recorded in this ping will have a category of ``sync``. The following +events are defined, categorized by the event method. + +sendcommand +----------- + +Records that Sync wrote a remote "command" to another client. These commands +cause that other client to take some action, such as resetting Sync on that +client, or opening a new URL. + +- object: The specific command being written. +- value: Not used (ie, ``undefined``) +- extra: An object with the following attributes: + + - deviceID: A GUID which identifies the device the command is being sent to. + - flowID: A GUID which uniquely identifies this command invocation. + +processcommand +-------------- + +Records that Sync processed a remote "command" previously sent by another +client. This is logically the "other end" of ``sendcommand``. + +- object: The specific command being processed. +- value: Not used (ie, ``undefined``) +- extra: An object with the following attributes: + + - deviceID: A GUID which identifies the device the command is being sent to. + - flowID: A GUID which uniquely identifies this command invocation. The value + for this GUID will be the same as the flowID sent to the client via + ``sendcommand``.
--- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -241,24 +241,29 @@ static inline void profiler_js_operation static inline double profiler_time() { return 0; } static inline double profiler_time(const mozilla::TimeStamp& aTime) { return 0; } static inline bool profiler_in_privacy_mode() { return false; } static inline void profiler_log(const char *str) {} static inline void profiler_log(const char *fmt, va_list args) {} +namespace mozilla { + class AutoProfilerRegister final MOZ_STACK_CLASS { - AutoProfilerRegister(const char* aName) {} +public: + explicit AutoProfilerRegister(const char* aName) {} private: AutoProfilerRegister(const AutoProfilerRegister&) = delete; AutoProfilerRegister& operator=(const AutoProfilerRegister&) = delete; }; +} // namespace mozilla + #else #include "GeckoProfilerImpl.h" #endif class MOZ_RAII GeckoProfilerInitRAII { public:
--- a/widget/gtk/gtk3drawing.cpp +++ b/widget/gtk/gtk3drawing.cpp @@ -1808,22 +1808,28 @@ moz_gtk_menu_separator_paint(cairo_t *cr } // See gtk_menu_item_draw() for reference. static gint moz_gtk_menu_item_paint(WidgetNodeType widget, cairo_t *cr, GdkRectangle* rect, GtkWidgetState* state, GtkTextDirection direction) { gint x, y, w, h; + guint minorVersion = gtk_get_minor_version(); + GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); - GtkStateFlags state_flags = GetStateFlagsFromGtkWidgetState(state); + // GTK versions prior to 3.8 render the background and frame only when not + // a separator and in hover prelight. + if (minorVersion < 8 && (widget == MOZ_GTK_MENUSEPARATOR || + !(state_flags & GTK_STATE_FLAG_PRELIGHT))) + return MOZ_GTK_SUCCESS; + GtkStyleContext* style = ClaimStyleContext(widget, direction, state_flags); - bool pre_3_6 = gtk_check_version(3, 6, 0) != nullptr; - if (pre_3_6) { + if (minorVersion < 6) { // GTK+ 3.4 saves the style context and adds the menubar class to // menubar children, but does each of these only when drawing, not // during layout. gtk_style_context_save(style); if (widget == MOZ_GTK_MENUBARITEM) { gtk_style_context_add_class(style, GTK_STYLE_CLASS_MENUBAR); } } @@ -1831,17 +1837,17 @@ moz_gtk_menu_item_paint(WidgetNodeType w x = rect->x; y = rect->y; w = rect->width; h = rect->height; gtk_render_background(style, cr, x, y, w, h); gtk_render_frame(style, cr, x, y, w, h); - if (pre_3_6) { + if (minorVersion < 6) { gtk_style_context_restore(style); } ReleaseStyleContext(style); return MOZ_GTK_SUCCESS; } static gint