author | Andreea Pavel <apavel@mozilla.com> |
Mon, 21 May 2018 12:50:31 +0300 | |
changeset 419156 | d193d9e81b9a58f348ca63ceaec0df9c08c6f37d |
parent 419155 | fb45edcf3d466e98667903baaad3c441950f144d (current diff) |
parent 419124 | dc1868d255be744a7d2d462216be205086cc60af (diff) |
child 419157 | 9c064950c349fc3ababcb266dd3e77819057cd86 |
push id | 34029 |
push user | shindli@mozilla.com |
push date | Mon, 21 May 2018 21:30:22 +0000 |
treeherder | mozilla-central@51f2535c7974 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 62.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/themes/webconsole.css +++ b/devtools/client/themes/webconsole.css @@ -501,40 +501,42 @@ a.learn-more-link.webconsole-learn-more- } /* This element contains the different toolbars in the console - primary, containing the clear messages button and the text search input. It can expand as much as it need. - filtered messages, containing the "X items hidden by filters" and the reset filters button. It should be on the same row than the primary bar if it fits there, or on its own 100% row if it is wrapped. + - close button, close the split console panel. This button will be displayed on righ-top of tool bar always. - secondary, containing the filter buttons (Error, Warning, …). It should be on its own 100% row. Basically here's what we can have : - ----------------------------------------------------------------------------------------------------------- - | Clear button - Open filter bar button - Filter Input | X items hidden by filters - Reset Filters button | - ----------------------------------------------------------------------------------------------------------- - | Error - Warning - Log - Info - Debug - CSS - Network - XHR | - ----------------------------------------------------------------------------------------------------------- + -------------------------------------------------------------------------------------------------------------------------- + | Clear button - Open filter bar button - Filter Input | X items hidden by filters - Reset Filters button | Close button | + -------------------------------------------------------------------------------------------------------------------------- + | Error - Warning - Log - Info - Debug - CSS - Network - XHR | + -------------------------------------------------------------------------------------------------------------------------- or - ------------------------------------------------------------------------------------ - | Clear button - Open filter bar button - Filter Input | - ------------------------------------------------------------------------------------ - | X items hidden by filters - Reset Filters button | - ------------------------------------------------------------------------------------ - | Error - Warning - Log - Info - Debug - CSS - Network - XHR | - ------------------------------------------------------------------------------------ + --------------------------------------------------------------------------------------------------- + | Clear button - Open filter bar button - Filter Input | Close button | + --------------------------------------------------------------------------------------------------- + | X items hidden by filters - Reset Filters button | + --------------------------------------------------------------------------------------------------- + | Error - Warning - Log - Info - Debug - CSS - Network - XHR | + --------------------------------------------------------------------------------------------------- */ .webconsole-filteringbar-wrapper { - display: flex; + display: grid; grid-row: 1 / 2; + grid-template-columns: 1fr auto auto; /* Wrap so the "Hidden messages" bar can go to its own row if needed */ flex-wrap: wrap; --primary-toolbar-height: 29px; } .webconsole-filterbar-primary { display: flex; /* We want the toolbar (which contain the text search input) to fit @@ -544,16 +546,24 @@ a.learn-more-link.webconsole-learn-more- min-height: var(--primary-toolbar-height); } .devtools-toolbar.webconsole-filterbar-secondary { display: flex; width: 100%; align-items: center; -moz-user-select: none; + grid-column: 1 / -1; +} + +.split-console-close-button-wrapper { + min-height: var(--primary-toolbar-height); + /* We will need to display the close button in the right-top always. */ + grid-column: -1 / -2; + grid-row: 1 / 2; } .webconsole-filterbar-primary .devtools-plaininput { flex: 1 1 100%; align-self: stretch; margin-left: 1px; padding-inline-start: 4px; border: 1px solid transparent; @@ -583,16 +593,27 @@ a.learn-more-link.webconsole-learn-more- justify-content: flex-end; color: var(--theme-comment); text-align: end; align-items: center; min-height: var(--primary-toolbar-height); line-height: var(--primary-toolbar-height); } +@media (max-width: 965px) { + /* This media query will make filtered message element to be displayed in new + line. This width is based on greek localized size since it will longer than + other localized strings. */ + .webconsole-filterbar-filtered-messages { + grid-row: 2 / 3; + grid-column: 1 / -1; + justify-self: stretch; + } +} + .webconsole-filterbar-filtered-messages .filter-message-text { font-style: italic; -moz-user-select: none; } .webconsole-filterbar-filtered-messages .reset-filters-button { margin-inline-start: 0.5em; } @@ -1039,8 +1060,12 @@ html[dir="rtl"] .webconsole-output-wrapp .sidebar-contents .object-inspector { min-width: 100%; } .theme-twisty { cursor: default; } + +#split-console-close-button::before { + background-image: var(--close-button-image); +}
--- a/devtools/client/webconsole/actions/ui.js +++ b/devtools/client/webconsole/actions/ui.js @@ -13,16 +13,17 @@ const { FILTER_BAR_TOGGLE, INITIALIZE, PERSIST_TOGGLE, PREFS, SELECT_NETWORK_MESSAGE_TAB, SIDEBAR_CLOSE, SHOW_OBJECT_IN_SIDEBAR, TIMESTAMPS_TOGGLE, + SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE, } = require("devtools/client/webconsole/constants"); function filterBarToggle(show) { return (dispatch, getState, {prefsService}) => { dispatch({ type: FILTER_BAR_TOGGLE, }); const {filterBarVisible} = getAllUi(getState()); @@ -61,16 +62,23 @@ function initialize() { } function sidebarClose(show) { return { type: SIDEBAR_CLOSE, }; } +function splitConsoleCloseButtonToggle(shouldDisplayButton) { + return { + type: SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE, + shouldDisplayButton, + }; +} + function showObjectInSidebar(actorId, messageId) { return (dispatch, getState) => { let { parameters } = getMessage(getState(), messageId); if (Array.isArray(parameters)) { for (let parameter of parameters) { if (parameter.actor === actorId) { dispatch({ type: SHOW_OBJECT_IN_SIDEBAR, @@ -86,9 +94,10 @@ function showObjectInSidebar(actorId, me module.exports = { filterBarToggle, initialize, persistToggle, selectNetworkMessageTab, sidebarClose, showObjectInSidebar, timestampsToggle, + splitConsoleCloseButtonToggle, };
--- a/devtools/client/webconsole/components/App.js +++ b/devtools/client/webconsole/components/App.js @@ -37,16 +37,17 @@ class App extends Component { static get propTypes() { return { attachRefToHud: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, hud: PropTypes.object.isRequired, notifications: PropTypes.object, onFirstMeaningfulPaint: PropTypes.func.isRequired, serviceContainer: PropTypes.object.isRequired, + closeSplitConsole: PropTypes.func.isRequired, }; } constructor(props) { super(props); this.onPaste = this.onPaste.bind(this); } @@ -115,16 +116,17 @@ class App extends Component { render() { const { attachRefToHud, hud, notifications, onFirstMeaningfulPaint, serviceContainer, + closeSplitConsole, } = this.props; // Render the entire Console panel. The panel consists // from the following parts: // * FilterBar - Buttons & free text for content filtering // * Content - List of logs & messages // * SideBar - Object inspector // * NotificationBox - Notifications for JSTerm (self-xss warning at the moment) @@ -134,17 +136,18 @@ class App extends Component { className: "webconsole-output-wrapper", ref: node => { this.node = node; }}, FilterBar({ hidePersistLogsCheckbox: hud.isBrowserConsole, serviceContainer: { attachRefToHud - } + }, + closeSplitConsole }), ConsoleOutput({ serviceContainer, onFirstMeaningfulPaint, }), SideBar({ serviceContainer, }),
--- a/devtools/client/webconsole/components/FilterBar.js +++ b/devtools/client/webconsole/components/FilterBar.js @@ -28,16 +28,18 @@ class FilterBar extends Component { filter: PropTypes.object.isRequired, serviceContainer: PropTypes.shape({ attachRefToHud: PropTypes.func.isRequired, }).isRequired, filterBarVisible: PropTypes.bool.isRequired, persistLogs: PropTypes.bool.isRequired, hidePersistLogsCheckbox: PropTypes.bool.isRequired, filteredMessagesCount: PropTypes.object.isRequired, + closeButtonVisible: PropTypes.bool, + closeSplitConsole: PropTypes.func, }; } static get defaultProps() { return { hidePersistLogsCheckbox: false, }; } @@ -57,35 +59,47 @@ class FilterBar extends Component { componentDidMount() { this.props.serviceContainer.attachRefToHud( "filterBox", this.wrapperNode.querySelector(".text-filter") ); } shouldComponentUpdate(nextProps, nextState) { - if (nextProps.filter !== this.props.filter) { + const { + filter, + filterBarVisible, + persistLogs, + filteredMessagesCount, + closeButtonVisible, + } = this.props; + + if (nextProps.filter !== filter) { return true; } - if (nextProps.filterBarVisible !== this.props.filterBarVisible) { + if (nextProps.filterBarVisible !== filterBarVisible) { return true; } - if (nextProps.persistLogs !== this.props.persistLogs) { + if (nextProps.persistLogs !== persistLogs) { return true; } if ( - JSON.stringify(nextProps.filteredMessagesCount) - !== JSON.stringify(this.props.filteredMessagesCount) + JSON.stringify(nextProps.filteredMessagesCount) !== + JSON.stringify(filteredMessagesCount) ) { return true; } + if (nextProps.closeButtonVisible != closeButtonVisible) { + return true; + } + return false; } onClickMessagesClear() { this.props.dispatch(actions.messagesClear()); } onClickFilterBarToggle() { @@ -223,16 +237,17 @@ class FilterBar extends Component { render() { const { filter, filterBarVisible, persistLogs, filteredMessagesCount, hidePersistLogsCheckbox, + closeSplitConsole, } = this.props; let children = [ dom.div({ className: "devtools-toolbar webconsole-filterbar-primary", key: "primary-bar", }, dom.button({ @@ -264,16 +279,31 @@ class FilterBar extends Component { }), ) ]; if (filteredMessagesCount.global > 0) { children.push(this.renderFilteredMessagesBar()); } + if (this.props.closeButtonVisible) { + children.push(dom.div( + { + className: "devtools-toolbar split-console-close-button-wrapper" + }, + dom.button({ + id: "split-console-close-button", + className: "devtools-button", + onClick: () => { + closeSplitConsole(); + }, + }) + )); + } + if (filterBarVisible) { children.push(this.renderFiltersConfigBar()); } return ( dom.div({ className: "webconsole-filteringbar-wrapper", ref: node => { @@ -287,12 +317,13 @@ class FilterBar extends Component { function mapStateToProps(state) { let uiState = getAllUi(state); return { filter: getAllFilters(state), filterBarVisible: uiState.filterBarVisible, persistLogs: uiState.persistLogs, filteredMessagesCount: getFilteredMessagesCount(state), + closeButtonVisible: uiState.closeButtonVisible, }; } module.exports = connect(mapStateToProps)(FilterBar);
--- a/devtools/client/webconsole/constants.js +++ b/devtools/client/webconsole/constants.js @@ -24,16 +24,17 @@ const actionTypes = { PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR", REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR", SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB", SIDEBAR_CLOSE: "SIDEBAR_CLOSE", SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR", TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE", APPEND_NOTIFICATION: "APPEND_NOTIFICATION", REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION", + SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE", }; const prefs = { PREFS: { // Filter preferences only have the suffix since they can be used either for the // webconsole or the browser console. FILTER: { ERROR: "filter.error",
--- a/devtools/client/webconsole/new-console-output-wrapper.js +++ b/devtools/client/webconsole/new-console-output-wrapper.js @@ -208,16 +208,17 @@ NewConsoleOutputWrapper.prototype = { }); } const app = App({ attachRefToHud, serviceContainer, hud, onFirstMeaningfulPaint: resolve, + closeSplitConsole: this.closeSplitConsole.bind(this), }); // Render the root Application component. let provider = createElement(Provider, { store }, app); this.body = ReactDOM.render(provider, this.parentNode); }); }, @@ -340,16 +341,21 @@ NewConsoleOutputWrapper.prototype = { dispatchRequestUpdate: function(id, data) { this.batchedRequestUpdates({ id, data }); }, dispatchSidebarClose: function() { store.dispatch(actions.sidebarClose()); }, + dispatchSplitConsoleCloseButtonToggle: function() { + store.dispatch(actions.splitConsoleCloseButtonToggle( + this.toolbox && this.toolbox.currentToolId !== "webconsole")); + }, + batchedMessageUpdates: function(info) { this.queuedMessageUpdates.push(info); this.setTimeoutIfNeeded(); }, batchedRequestUpdates: function(message) { this.queuedRequestUpdates.push(message); this.setTimeoutIfNeeded(); @@ -412,13 +418,18 @@ NewConsoleOutputWrapper.prototype = { done(); }, 50); }); }, // Should be used for test purpose only. getStore: function() { return store; + }, + + // Called by pushing close button. + closeSplitConsole() { + this.toolbox.closeSplitConsole(); } }; // Exports from this module module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-webconsole.js +++ b/devtools/client/webconsole/new-webconsole.js @@ -43,16 +43,17 @@ const PREF_SIDEBAR_ENABLED = "devtools.w function NewWebConsoleFrame(webConsoleOwner) { this.owner = webConsoleOwner; this.hudId = this.owner.hudId; this.isBrowserConsole = this.owner._browserConsole; this.window = this.owner.iframeWindow; this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); this._onPanelSelected = this._onPanelSelected.bind(this); + this._onChangeSplitConsoleState = this._onChangeSplitConsoleState.bind(this); EventEmitter.decorate(this); } NewWebConsoleFrame.prototype = { /** * Getter for the debugger WebConsoleClient. * @type object */ @@ -98,16 +99,18 @@ NewWebConsoleFrame.prototype = { if (this.jsterm) { this.jsterm.destroy(); this.jsterm = null; } let toolbox = gDevTools.getToolbox(this.owner.target); if (toolbox) { toolbox.off("webconsole-selected", this._onPanelSelected); + toolbox.off("split-console", this._onChangeSplitConsoleState); + toolbox.off("select", this._onChangeSplitConsoleState); } this.window = this.owner = this.newConsoleOutput = null; let onDestroy = () => { this._destroyer.resolve(null); }; if (this.proxy) { @@ -207,16 +210,18 @@ NewWebConsoleFrame.prototype = { // Toggle the timestamp on preference change Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged); this._onToolboxPrefChanged(); this._initShortcuts(); if (toolbox) { toolbox.on("webconsole-selected", this._onPanelSelected); + toolbox.on("split-console", this._onChangeSplitConsoleState); + toolbox.on("select", this._onChangeSplitConsoleState); } }, _initShortcuts: function() { let shortcuts = new KeyShortcuts({ window: this.window }); @@ -291,16 +296,20 @@ NewWebConsoleFrame.prototype = { * Sets the focus to JavaScript input field when the web console tab is * selected or when there is a split console present. * @private */ _onPanelSelected: function() { this.jsterm.focus(); }, + _onChangeSplitConsoleState: function() { + this.newConsoleOutput.dispatchSplitConsoleCloseButtonToggle(); + }, + /** * Handler for the tabNavigated notification. * * @param string event * Event name. * @param object packet * Notification packet received from the server. */
--- a/devtools/client/webconsole/reducers/ui.js +++ b/devtools/client/webconsole/reducers/ui.js @@ -9,30 +9,32 @@ const { FILTER_BAR_TOGGLE, INITIALIZE, PERSIST_TOGGLE, SELECT_NETWORK_MESSAGE_TAB, SIDEBAR_CLOSE, SHOW_OBJECT_IN_SIDEBAR, TIMESTAMPS_TOGGLE, MESSAGES_CLEAR, + SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE, } = require("devtools/client/webconsole/constants"); const { PANELS, } = require("devtools/client/netmonitor/src/constants"); const UiState = (overrides) => Object.freeze(Object.assign({ filterBarVisible: false, initialized: false, networkMessageActiveTabId: PANELS.HEADERS, persistLogs: false, sidebarVisible: false, timestampsVisible: true, - gripInSidebar: null + gripInSidebar: null, + closeButtonVisible: false, }, overrides)); function ui(state = UiState(), action) { switch (action.type) { case FILTER_BAR_TOGGLE: return Object.assign({}, state, {filterBarVisible: !state.filterBarVisible}); case PERSIST_TOGGLE: return Object.assign({}, state, {persistLogs: !state.persistLogs}); @@ -49,16 +51,18 @@ function ui(state = UiState(), action) { return Object.assign({}, state, {initialized: true}); case MESSAGES_CLEAR: return Object.assign({}, state, {sidebarVisible: false, gripInSidebar: null}); case SHOW_OBJECT_IN_SIDEBAR: if (action.grip === state.gripInSidebar) { return state; } return Object.assign({}, state, {sidebarVisible: true, gripInSidebar: action.grip}); + case SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: + return Object.assign({}, state, {closeButtonVisible: action.shouldDisplayButton}); } return state; } module.exports = { UiState, ui,
--- a/devtools/client/webconsole/test/mochitest/browser.ini +++ b/devtools/client/webconsole/test/mochitest/browser.ini @@ -337,16 +337,17 @@ subsuite = clipboard [browser_webconsole_shows_reqs_in_netmonitor.js] [browser_webconsole_sidebar_object_expand_when_message_pruned.js] [browser_webconsole_sourcemap_css.js] [browser_webconsole_sourcemap_error.js] [browser_webconsole_sourcemap_invalid.js] [browser_webconsole_sourcemap_nosource.js] [browser_webconsole_split.js] skip-if = (os == 'mac') || (os == 'linux' && !debug && bits == 64) || (os == 'win') # Bug 1454123 disabled on OS X, Windows and Linux64 for frequent failures +[browser_webconsole_split_close_button.js] [browser_webconsole_split_escape_key.js] [browser_webconsole_split_focus.js] [browser_webconsole_split_persist.js] [browser_webconsole_stacktrace_location_debugger_link.js] [browser_webconsole_stacktrace_location_scratchpad_link.js] [browser_webconsole_strict_mode_errors.js] [browser_webconsole_string.js] [browser_webconsole_time_methods.js]
new file mode 100644 --- /dev/null +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split_close_button.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for close button of " + + "split console"; + +add_task(async function() { + let toolbox = await openNewTabAndToolbox(TEST_URI, "inspector"); + + info("Check the split console toolbar has a close button."); + + let onSplitConsoleReady = toolbox.once("webconsole-ready"); + toolbox.toggleSplitConsole(); + await onSplitConsoleReady; + + let closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console has close button."); + + info("Check we can reopen split console after closing split console by using " + + "the close button"); + + let onSplitConsoleChange = toolbox.once("split-console"); + closeButton.click(); + await onSplitConsoleChange; + ok(!toolbox.splitConsole, "The split console has been closed."); + + onSplitConsoleChange = toolbox.once("split-console"); + toolbox.toggleSplitConsole(); + await onSplitConsoleChange; + + ok(toolbox.splitConsole, "The split console has been displayed."); + closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console has the close button after reopening."); + + info("Check the close button is not displayed on console panel."); + + await toolbox.selectTool("webconsole"); + closeButton = getCloseButton(toolbox); + ok(!closeButton, "The console panel should not have the close button."); + + info("The split console has the close button if back to the inspector."); + + await toolbox.selectTool("inspector"); + ok(toolbox.splitConsole, "The split console has been displayed with inspector."); + closeButton = getCloseButton(toolbox); + ok(closeButton, "The split console on the inspector has the close button."); +}); + +function getCloseButton(toolbox) { + let hud = toolbox.getPanel("webconsole").hud; + let doc = hud.ui.outputNode.ownerDocument; + return doc.getElementById("split-console-close-button"); +}
--- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -1817,16 +1817,17 @@ void HTMLMediaElement::AbortExistingLoad mTags = nullptr; if (mNetworkState != NETWORK_EMPTY) { NS_ASSERTION(!mDecoder && !mSrcStream, "How did someone setup a new stream/decoder already?"); // ChangeNetworkState() will call UpdateAudioChannelPlayingState() // indirectly which depends on mPaused. So we need to update mPaused first. if (!mPaused) { mPaused = true; + DispatchAsyncEvent(NS_LITERAL_STRING("pause")); RejectPromises(TakePendingPlayPromises(), NS_ERROR_DOM_MEDIA_ABORT_ERR); } ChangeNetworkState(NETWORK_EMPTY); ChangeReadyState(HAVE_NOTHING); //TODO: Apply the rules for text track cue rendering Bug 865407 if (mTextTrackManager) { mTextTrackManager->GetTextTracks()->SetCuesInactive(); @@ -4073,17 +4074,16 @@ HTMLMediaElement::PlayInternal(ErrorResu UpdateSrcMediaStreamPlaying(); // Once play() has been called in a user generated event handler, // it is allowed to autoplay. Note: we can reach here when not in // a user generated event handler if our readyState has not yet // reached HAVE_METADATA. mIsBlessed |= EventStateManager::IsHandlingUserInput(); - // TODO: If the playback has ended, then the user agent must set // seek to the effective start. // 4.8.12.8 - Step 6: // If the media element's paused attribute is true, run the following steps: if (oldPaused) { // 6.1. Change the value of paused to false. (Already done.) // This step is uplifted because the "block-media-playback" feature needs @@ -4109,20 +4109,22 @@ HTMLMediaElement::PlayInternal(ErrorResu case HAVE_METADATA: case HAVE_CURRENT_DATA: FireTimeUpdate(false); DispatchAsyncEvent(NS_LITERAL_STRING("waiting")); break; case HAVE_FUTURE_DATA: case HAVE_ENOUGH_DATA: FireTimeUpdate(false); - NotifyAboutPlaying(); + if (!mAttemptPlayUponLoadedMetadata) { + NotifyAboutPlaying(); + } break; } - } else if (mReadyState >= HAVE_FUTURE_DATA) { + } else if (mReadyState >= HAVE_FUTURE_DATA && !mAttemptPlayUponLoadedMetadata) { // 7. Otherwise, if the media element's readyState attribute has the value // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and // queue a task to resolve pending play promises with the result. AsyncResolvePendingPlayPromises(); } // 8. Set the media element's autoplaying flag to false. (Already done.) @@ -5887,16 +5889,27 @@ HTMLMediaElement::ChangeReadyState(nsMed DDLOG(DDLogCategory::Property, "ready_state", gReadyStateToString[aState]); if (mNetworkState == NETWORK_EMPTY) { return; } UpdateAudioChannelPlayingState(); + if (oldState < HAVE_METADATA && + mReadyState >= HAVE_METADATA && + mAttemptPlayUponLoadedMetadata) { + mAttemptPlayUponLoadedMetadata = false; + if (!mPaused && !IsAllowedToPlay()) { + mPaused = true; + DispatchAsyncEvent(NS_LITERAL_STRING("pause")); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR); + } + } + // Handle raising of "waiting" event during seek (see 4.8.10.9) // or // 4.8.12.7 Ready states: // "If the previous ready state was HAVE_FUTURE_DATA or more, and the new // ready state is HAVE_CURRENT_DATA or less // If the media element was potentially playing before its readyState // attribute changed to a value lower than HAVE_FUTURE_DATA, and the element // has not ended playback, and playback has not stopped due to errors, @@ -5919,25 +5932,18 @@ HTMLMediaElement::ChangeReadyState(nsMed DispatchAsyncEvent(NS_LITERAL_STRING("loadeddata")); mLoadedDataFired = true; } if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) { DispatchAsyncEvent(NS_LITERAL_STRING("canplay")); if (!mPaused) { - if (mAttemptPlayUponLoadedMetadata && mDecoder) { - mAttemptPlayUponLoadedMetadata = false; - if (IsAllowedToPlay()) { - mDecoder->Play(); - } else { - mPaused = true; - DispatchAsyncEvent(NS_LITERAL_STRING("pause")); - AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR); - } + if (mDecoder) { + mDecoder->Play(); } NotifyAboutPlaying(); } } CheckAutoplayDataReady(); if (oldState < HAVE_ENOUGH_DATA &&
--- a/dom/media/test/file_autoplay_policy_play_before_loadedmetadata.html +++ b/dom/media/test/file_autoplay_policy_play_before_loadedmetadata.html @@ -24,24 +24,39 @@ async function testPlayBeforeLoadedMetata(testCase, parent_window) { info("testPlayBeforeLoadedMetata: " + testCase.resource); let element = document.createElement("video"); element.preload = "auto"; element.src = testCase.resource; document.body.appendChild(element); + is(element.paused, true, testCase.resource + " - should start out paused."); + let playEventFired = false; once(element, "play").then(() => { playEventFired = true; }); + let playingEventFired = false; + once(element, "playing").then(() => { playingEventFired = true;}); let pauseEventFired = false; once(element, "pause").then(() => { pauseEventFired = true; }); + let played = await element.play().then(() => true, () => false); - let msg = testCase.resource + " should " + (!testCase.shouldPlay ? "not " : "") + "play"; - is(played, testCase.shouldPlay, msg); + + // Wait for one round through the event loop. This gives any tasks + // inside Gecko enqueued to dispatch events a chance to run. + // Specifically the "playing" event, if it's erronously fired. + await new Promise((resolve, reject) => { + setTimeout(resolve, 0); + }); + + let playingEventMsg = testCase.resource + " should " + (!testCase.shouldPlay ? "not " : "") + " fire playing"; + is(playingEventFired, testCase.shouldPlay, playingEventMsg); + let playMsg = testCase.resource + " should " + (!testCase.shouldPlay ? "not " : "") + "play"; + is(played, testCase.shouldPlay, playMsg); is(playEventFired, true, testCase.resource + " - we should always get a play event"); is(pauseEventFired, !testCase.shouldPlay, testCase.resource + " - if we shouldn't play, we should get a pause event"); removeNodeAndSource(element); } nextWindowMessage().then( async (event) => { await testPlayBeforeLoadedMetata(event.data, event.source);
--- a/toolkit/components/telemetry/TelemetryHistogram.cpp +++ b/toolkit/components/telemetry/TelemetryHistogram.cpp @@ -13,16 +13,20 @@ #include "nsBaseHashtable.h" #include "nsClassHashtable.h" #include "nsITelemetry.h" #include "nsPrintfCString.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/gfx/GPUProcessManager.h" #include "mozilla/Atomics.h" +#if defined(MOZ_TELEMETRY_GECKOVIEW) +// This is only used on GeckoView. +#include "mozilla/JSONWriter.h" +#endif #include "mozilla/StartupTimeline.h" #include "mozilla/StaticMutex.h" #include "mozilla/Unused.h" #include "TelemetryCommon.h" #include "TelemetryHistogram.h" #include "TelemetryScalar.h" #include "ipc/TelemetryIPCAccumulator.h" @@ -31,27 +35,29 @@ #include <limits> using base::Histogram; using base::BooleanHistogram; using base::CountHistogram; using base::FlagHistogram; using base::LinearHistogram; +using mozilla::MakeTuple; using mozilla::StaticMutex; using mozilla::StaticMutexAutoLock; using mozilla::Telemetry::HistogramAccumulation; using mozilla::Telemetry::KeyedHistogramAccumulation; using mozilla::Telemetry::HistogramID; using mozilla::Telemetry::ProcessID; using mozilla::Telemetry::HistogramCount; using mozilla::Telemetry::Common::LogToBrowserConsole; using mozilla::Telemetry::Common::RecordedProcessType; using mozilla::Telemetry::Common::AutoHashtable; using mozilla::Telemetry::Common::GetNameForProcessID; +using mozilla::Telemetry::Common::GetIDForProcessName; using mozilla::Telemetry::Common::IsExpiredVersion; using mozilla::Telemetry::Common::CanRecordDataset; using mozilla::Telemetry::Common::CanRecordProduct; using mozilla::Telemetry::Common::SupportedProduct; using mozilla::Telemetry::Common::IsInDataset; using mozilla::Telemetry::Common::ToJSString; namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator; @@ -142,32 +148,43 @@ struct HistogramInfo { SupportedProduct products; const char *name() const; const char *expiration() const; nsresult label_id(const char* label, uint32_t* labelId) const; bool allows_key(const nsACString& key) const; }; -enum reflectStatus { - REFLECT_OK, - REFLECT_FAILURE -}; - -// Struct used to keep information about the histograms for which a +// Structs used to keep information about the histograms for which a // snapshot should be created. struct HistogramSnapshotData { nsTArray<Histogram::Sample> mBucketRanges; nsTArray<Histogram::Count> mBucketCounts; int64_t mSampleSum; // Same type as Histogram::SampleSet::sum_ }; +struct HistogramSnapshotInfo { + HistogramSnapshotData data; + HistogramID histogramID; +}; + +typedef mozilla::Vector<HistogramSnapshotInfo> HistogramSnapshotsArray; +typedef mozilla::Vector<HistogramSnapshotsArray> HistogramProcessSnapshotsArray; + // The following is used to handle snapshot information for keyed histograms. typedef nsDataHashtable<nsCStringHashKey, HistogramSnapshotData> KeyedHistogramSnapshotData; +struct KeyedHistogramSnapshotInfo { + KeyedHistogramSnapshotData data; + HistogramID histogramId; +}; + +typedef mozilla::Vector<KeyedHistogramSnapshotInfo> KeyedHistogramSnapshotsArray; +typedef mozilla::Vector<KeyedHistogramSnapshotsArray> KeyedHistogramProcessSnapshotsArray; + class KeyedHistogram { public: KeyedHistogram(HistogramID id, const HistogramInfo& info); ~KeyedHistogram(); nsresult GetHistogram(const nsCString& name, Histogram** histogram); Histogram* GetHistogram(const nsCString& name); uint32_t GetHistogramType() const { return mHistogramInfo.histogramType; } nsresult GetJSKeys(JSContext* cx, JS::CallArgs& args); @@ -177,16 +194,18 @@ public: nsresult GetSnapshot(const StaticMutexAutoLock& aLock, KeyedHistogramSnapshotData& aSnapshot, bool aClearSubsession); nsresult Add(const nsCString& key, uint32_t aSample, ProcessID aProcessType); void Clear(); HistogramID GetHistogramID() const { return mId; } + bool IsEmpty() const { return mHistogramMap.IsEmpty(); } + private: typedef nsBaseHashtableET<nsCStringHashKey, Histogram*> KeyedHistogramEntry; typedef AutoHashtable<KeyedHistogramEntry> KeyedHistogramMapType; KeyedHistogramMapType mHistogramMap; const HistogramID mId; const HistogramInfo& mHistogramInfo; }; @@ -407,16 +426,26 @@ internal_CanRecordBase() { return gCanRecordBase; } bool internal_CanRecordExtended() { return gCanRecordExtended; } +bool +internal_AttemptedGPUProcess() { + // Check if it was tried to launch a process. + bool attemptedGPUProcess = false; + if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) { + attemptedGPUProcess = gpm->AttemptedGPUProcess(); + } + return attemptedGPUProcess; +} + // Note: this is completely unrelated to mozilla::IsEmpty. bool internal_IsEmpty(const Histogram *h) { return h->is_empty(); } bool @@ -741,19 +770,92 @@ internal_ShouldReflectHistogram(Histogra // This has historical reasons, changing this will require downstream changes. // The cheaper path here is to just deprecate flag histograms in favor // of scalars. uint32_t type = gHistogramInfos[id].histogramType; if (internal_IsEmpty(h) && type != nsITelemetry::HISTOGRAM_FLAG) { return false; } + // Don't reflect the histogram if it's not allowed in this product. + if (!CanRecordProduct(gHistogramInfos[id].products)) { + return false; + } + return true; } +/** + * Helper function to get a snapshot of the histograms. + * + * @param {aLock} the lock proof. + * @param {aDataset} the dataset for which the snapshot is being requested. + * @param {aClearSubsession} whether or not to clear the data after + * taking the snapshot. + * @param {aIncludeGPU} whether or not to include data for the GPU. + * @param {aOutSnapshot} the container in which the snapshot data will be stored. + * @return {nsresult} NS_OK if the snapshot was successfully taken or + * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory. + */ +nsresult +internal_GetHistogramsSnapshot(const StaticMutexAutoLock& aLock, + unsigned int aDataset, + bool aClearSubsession, + bool aIncludeGPU, + HistogramProcessSnapshotsArray& aOutSnapshot) +{ + if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); ++process) { + HistogramSnapshotsArray& hArray = aOutSnapshot[process]; + + for (size_t i = 0; i < HistogramCount; ++i) { + const HistogramInfo& info = gHistogramInfos[i]; + if (info.keyed) { + continue; + } + + HistogramID id = HistogramID(i); + + if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || + ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) { + continue; + } + + if (!IsInDataset(info.dataset, aDataset)) { + continue; + } + + bool shouldInstantiate = + info.histogramType == nsITelemetry::HISTOGRAM_FLAG; + Histogram* h = internal_GetHistogramById(id, ProcessID(process), + shouldInstantiate); + if (!h || internal_IsExpired(h) || !internal_ShouldReflectHistogram(h, id)) { + continue; + } + + HistogramSnapshotData snapshotData; + if (NS_FAILED(internal_GetHistogramAndSamples(aLock, h, snapshotData))) { + continue; + } + + if (!hArray.emplaceBack(HistogramSnapshotInfo{snapshotData, id})) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (aClearSubsession) { + h->Clear(); + } + } + } + return NS_OK; +} + } // namespace //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// // // PRIVATE: class KeyedHistogram and internal_ReflectKeyedHistogram namespace { @@ -967,16 +1069,84 @@ KeyedHistogram::GetSnapshot(const Static if (aClearSubsession) { Clear(); } return NS_OK; } + +/** + * Helper function to get a snapshot of the keyed histograms. + * + * @param {aLock} the lock proof. + * @param {aDataset} the dataset for which the snapshot is being requested. + * @param {aClearSubsession} whether or not to clear the data after + * taking the snapshot. + * @param {aIncludeGPU} whether or not to include data for the GPU. + * @param {aOutSnapshot} the container in which the snapshot data will be stored. + * @param {aSkipEmpty} whether or not to skip empty keyed histograms from the + * snapshot. Can't always assume "true" for consistency with the other + * callers. + * @return {nsresult} NS_OK if the snapshot was successfully taken or + * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory. + */ +nsresult +internal_GetKeyedHistogramsSnapshot(const StaticMutexAutoLock& aLock, + unsigned int aDataset, + bool aClearSubsession, + bool aIncludeGPU, + KeyedHistogramProcessSnapshotsArray& aOutSnapshot, + bool aSkipEmpty = false) +{ + if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); ++process) { + KeyedHistogramSnapshotsArray& hArray = aOutSnapshot[process]; + + for (size_t i = 0; i < HistogramCount; ++i) { + HistogramID id = HistogramID(i); + const HistogramInfo& info = gHistogramInfos[id]; + if (!info.keyed) { + continue; + } + + if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || + ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) { + continue; + } + + if (!IsInDataset(info.dataset, aDataset)) { + continue; + } + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, + ProcessID(process), + /* instantiate = */ false); + if (!keyed || (aSkipEmpty && keyed->IsEmpty())) { + continue; + } + + // Take a snapshot of the keyed histogram data! + KeyedHistogramSnapshotData snapshot; + if (!NS_SUCCEEDED(keyed->GetSnapshot(aLock, snapshot, aClearSubsession))) { + return NS_ERROR_FAILURE; + } + + if (!hArray.emplaceBack(KeyedHistogramSnapshotInfo{mozilla::Move(snapshot), id})) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + return NS_OK; +} + } // namespace //////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////// // // PRIVATE: thread-unsafe helpers for the external interface namespace { @@ -2215,94 +2385,44 @@ TelemetryHistogram::CreateHistogramSnaps JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx)); if (!root_obj) { return NS_ERROR_FAILURE; } aResult.setObject(*root_obj); // Include the GPU process in histogram snapshots only if we actually tried // to launch a process for it. - bool includeGPUProcess = false; - if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) { - includeGPUProcess = gpm->AttemptedGPUProcess(); - } - - // Struct used to keep information about the histograms for which a - // snapshot should be created - struct HistogramProcessInfo { - HistogramSnapshotData data; - HistogramID histogramID; - }; - - mozilla::Vector<mozilla::Vector<HistogramProcessInfo>> processHistArray; + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + HistogramProcessSnapshotsArray processHistArray; { - if (!processHistArray.resize(static_cast<uint32_t>(ProcessID::Count))) { - return NS_ERROR_OUT_OF_MEMORY; - } - StaticMutexAutoLock locker(gTelemetryHistogramMutex); - for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); ++process) { - mozilla::Vector<HistogramProcessInfo>& hArray = processHistArray[process]; - - for (size_t i = 0; i < HistogramCount; ++i) { - const HistogramInfo& info = gHistogramInfos[i]; - if (info.keyed) { - continue; - } - - HistogramID id = HistogramID(i); - - if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || - ((ProcessID(process) == ProcessID::Gpu) && !includeGPUProcess)) { - continue; - } - - if (!IsInDataset(info.dataset, aDataset)) { - continue; - } - - bool shouldInstantiate = - info.histogramType == nsITelemetry::HISTOGRAM_FLAG; - Histogram* h = internal_GetHistogramById(id, ProcessID(process), - shouldInstantiate); - if (!h || internal_IsExpired(h) || !internal_ShouldReflectHistogram(h, id)) { - continue; - } - - HistogramSnapshotData snapshotData; - if (NS_FAILED(internal_GetHistogramAndSamples(locker, h, snapshotData))) { - continue; - } - - if (!hArray.emplaceBack(HistogramProcessInfo{snapshotData, id})) { - return NS_ERROR_OUT_OF_MEMORY; - } - - if (aClearSubsession) { - h->Clear(); - } - } + nsresult rv = internal_GetHistogramsSnapshot(locker, + aDataset, + aClearSubsession, + includeGPUProcess, + processHistArray); + if (NS_FAILED(rv)) { + return rv; } } // Make the JS calls on the stashed histograms for every process for (uint32_t process = 0; process < processHistArray.length(); ++process) { JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx)); if (!processObject) { return NS_ERROR_FAILURE; } if (!JS_DefineProperty(aCx, root_obj, GetNameForProcessID(ProcessID(process)), processObject, JSPROP_ENUMERATE)) { return NS_ERROR_FAILURE; } - const mozilla::Vector<HistogramProcessInfo>& hArray = processHistArray[process]; - for (size_t i = 0; i < hArray.length(); ++i) { - const HistogramProcessInfo& hData = hArray[i]; + for (const HistogramSnapshotInfo& hData : processHistArray[process]) { HistogramID id = hData.histogramID; JS::Rooted<JSObject*> hobj(aCx, JS_NewPlainObject(aCx)); if (!hobj) { return NS_ERROR_FAILURE; } if (NS_FAILED(internal_ReflectHistogramAndSamples(aCx, @@ -2331,88 +2451,44 @@ TelemetryHistogram::GetKeyedHistogramSna JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); if (!obj) { return NS_ERROR_FAILURE; } aResult.setObject(*obj); // Include the GPU process in histogram snapshots only if we actually tried // to launch a process for it. - bool includeGPUProcess = false; - if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) { - includeGPUProcess = gpm->AttemptedGPUProcess(); - } - - struct KeyedHistogramProcessInfo { - KeyedHistogramSnapshotData data; - HistogramID histogramId; - }; + bool includeGPUProcess = internal_AttemptedGPUProcess(); // Get a snapshot of all the data while holding the mutex. - mozilla::Vector<mozilla::Vector<KeyedHistogramProcessInfo>> dataSnapshot; + KeyedHistogramProcessSnapshotsArray processHistArray; { - if (!dataSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) { - return NS_ERROR_OUT_OF_MEMORY; - } - StaticMutexAutoLock locker(gTelemetryHistogramMutex); - - for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); ++process) { - mozilla::Vector<KeyedHistogramProcessInfo>& hArray = dataSnapshot[process]; - - for (size_t i = 0; i < HistogramCount; ++i) { - HistogramID id = HistogramID(i); - const HistogramInfo& info = gHistogramInfos[id]; - if (!info.keyed) { - continue; - } - - if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) || - ((ProcessID(process) == ProcessID::Gpu) && !includeGPUProcess)) { - continue; - } - - if (!IsInDataset(info.dataset, aDataset)) { - continue; - } - - KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, - ProcessID(process), - /* instantiate = */ false); - if (!keyed) { - continue; - } - - // Take a snapshot of the keyed histogram data! - KeyedHistogramSnapshotData snapshot; - if (!NS_SUCCEEDED(keyed->GetSnapshot(locker, snapshot, aClearSubsession))) { - return NS_ERROR_FAILURE; - } - - if (!hArray.emplaceBack(KeyedHistogramProcessInfo{mozilla::Move(snapshot), id})) { - return NS_ERROR_OUT_OF_MEMORY; - } - } + nsresult rv = internal_GetKeyedHistogramsSnapshot(locker, + aDataset, + aClearSubsession, + includeGPUProcess, + processHistArray); + if (NS_FAILED(rv)) { + return rv; } } // Mirror the snapshot data to JS, now that we released the mutex. - for (uint32_t process = 0; process < dataSnapshot.length(); ++process) { - const mozilla::Vector<KeyedHistogramProcessInfo>& hArray = dataSnapshot[process]; - + for (uint32_t process = 0; process < processHistArray.length(); ++process) { JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx)); if (!processObject) { return NS_ERROR_FAILURE; } if (!JS_DefineProperty(aCx, obj, GetNameForProcessID(ProcessID(process)), processObject, JSPROP_ENUMERATE)) { return NS_ERROR_FAILURE; } - for (size_t i = 0; i < hArray.length(); ++i) { - const KeyedHistogramProcessInfo& hData = hArray[i]; + + for (const KeyedHistogramSnapshotInfo& hData : processHistArray[process]) { const HistogramInfo& info = gHistogramInfos[hData.histogramId]; JS::RootedObject snapshot(aCx, JS_NewPlainObject(aCx)); if (!snapshot) { return NS_ERROR_FAILURE; } if (!NS_SUCCEEDED(internal_ReflectKeyedHistogram(hData.data, info, aCx, snapshot))) { @@ -2439,8 +2515,617 @@ TelemetryHistogram::GetMapShallowSizesOf size_t TelemetryHistogram::GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) { StaticMutexAutoLock locker(gTelemetryHistogramMutex); // TODO return 0; } + +#if defined(MOZ_TELEMETRY_GECKOVIEW) +namespace base { +class PersistedSampleSet : public Histogram::SampleSet +{ +public: + explicit PersistedSampleSet(const nsTArray<Histogram::Count>& aCounts, + int64_t aSampleSum); +}; + +PersistedSampleSet::PersistedSampleSet(const nsTArray<Histogram::Count>& aCounts, + int64_t aSampleSum) +{ + // Initialize the data in the base class. See Histogram::SampleSet + // for the fields documentation. + const size_t numCounts = aCounts.Length(); + counts_.resize(numCounts); + for (size_t i = 0; i < numCounts; i++) { + counts_[i] = aCounts[i]; + redundant_count_ += aCounts[i]; + } + sum_ = aSampleSum; +}; +} // base (from ipc/chromium/src/base) + +namespace { +/** + * Helper function to write histogram properties to JSON. + * Please note that this needs to be called between + * StartObjectProperty/EndObject calls that mark the histogram's + * JSON creation. + */ +void +internal_ReflectHistogramToJSON(const HistogramSnapshotData& aSnapshot, + mozilla::JSONWriter& aWriter) +{ + aWriter.IntProperty("sum", aSnapshot.mSampleSum); + + // Fill the "counts" property. + aWriter.StartArrayProperty("counts"); + for (size_t i = 0; i < aSnapshot.mBucketCounts.Length(); i++) { + aWriter.IntElement(aSnapshot.mBucketCounts[i]); + } + aWriter.EndArray(); +} + +bool +internal_CanRecordHistogram(const HistogramID id, + ProcessID aProcessType) +{ + // Check if we are allowed to record the data. + if (!CanRecordDataset(gHistogramInfos[id].dataset, + internal_CanRecordBase(), + internal_CanRecordExtended())) { + return false; + } + + // Check if we're allowed to record in the given process. + if (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id)) { + return false; + } + + if (aProcessType != ProcessID::Parent + && !CanRecordInProcess(gHistogramInfos[id].record_in_processes, aProcessType)) { + return false; + } + + // Don't record if the current platform is not enabled + if (!CanRecordProduct(gHistogramInfos[id].products)) { + return false; + } + + return true; +} + +nsresult +internal_ParseHistogramData(JSContext* aCx, JS::HandleId aEntryId, + JS::HandleObject aContainerObj, nsACString& aOutName, + nsTArray<Histogram::Count>& aOutCountArray, int64_t& aOutSum) +{ + // Get the histogram name. + nsAutoJSString histogramName; + if (!histogramName.init(aCx, aEntryId)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + aOutName = NS_ConvertUTF16toUTF8(histogramName); + + // Get the data for this histogram. + JS::RootedValue histogramData(aCx); + if (!JS_GetPropertyById(aCx, aContainerObj, aEntryId, &histogramData)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!histogramData.isObject()) { + // Histogram data need to be an object. If that's not the case, skip it + // and try to load the rest of the data. + return NS_ERROR_FAILURE; + } + + // Get the "sum" property. + JS::RootedValue sumValue(aCx); + JS::RootedObject histogramObj(aCx, &histogramData.toObject()); + if (!JS_GetProperty(aCx, histogramObj, "sum", &sumValue)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!JS::ToInt64(aCx, sumValue, &aOutSum)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Get the "counts" array. + JS::RootedValue countsArray(aCx); + bool countsIsArray = false; + if (!JS_GetProperty(aCx, histogramObj, "counts", &countsArray) + || !JS_IsArrayObject(aCx, countsArray, &countsIsArray)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + if (!countsIsArray) { + // The "counts" property needs to be an array. If this is not the case, + // skip this histogram. + return NS_ERROR_FAILURE; + } + + // Get the length of the array. + uint32_t countsLen = 0; + JS::RootedObject countsArrayObj(aCx, &countsArray.toObject()); + if (!JS_GetArrayLength(aCx, countsArrayObj, &countsLen)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Parse the "counts" in the array. + for (uint32_t arrayIdx = 0; arrayIdx < countsLen; arrayIdx++) { + JS::RootedValue elementValue(aCx); + int countAsInt = 0; + if (!JS_GetElement(aCx, countsArrayObj, arrayIdx, &elementValue) + || !JS::ToInt32(aCx, elementValue, &countAsInt)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + aOutCountArray.AppendElement(countAsInt); + } + + return NS_OK; +} + +} // Anonymous namespace + +nsresult +TelemetryHistogram::SerializeHistograms(mozilla::JSONWriter& aWriter) +{ + MOZ_ASSERT(XRE_IsParentProcess(), "Only save histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + // Take a snapshot of the histograms. + HistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + // We always request the "opt-in"/"prerelease" dataset: we internally + // record the right subset, so this will only return "prerelease" if + // it was recorded. + if (NS_FAILED(internal_GetHistogramsSnapshot(locker, + nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN, + false /* aClearSubsession */, + includeGPUProcess, + processHistArray))) { + return NS_ERROR_FAILURE; + } + } + + + + // Make the JSON calls on the stashed histograms for every process + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + aWriter.StartObjectProperty(GetNameForProcessID(ProcessID(process))); + + for (const HistogramSnapshotInfo& hData : processHistArray[process]) { + HistogramID id = hData.histogramID; + + aWriter.StartObjectProperty(gHistogramInfos[id].name()); + internal_ReflectHistogramToJSON(hData.data, aWriter); + aWriter.EndObject(); + } + aWriter.EndObject(); + } + + return NS_OK; +} + +nsresult +TelemetryHistogram::SerializeKeyedHistograms(mozilla::JSONWriter& aWriter) +{ + MOZ_ASSERT(XRE_IsParentProcess(), "Only save keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Include the GPU process in histogram snapshots only if we actually tried + // to launch a process for it. + bool includeGPUProcess = internal_AttemptedGPUProcess(); + + // Take a snapshot of the keyed histograms. + KeyedHistogramProcessSnapshotsArray processHistArray; + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + // We always request the "opt-in"/"prerelease" dataset: we internally + // record the right subset, so this will only return "prerelease" if + // it was recorded. + if (NS_FAILED(internal_GetKeyedHistogramsSnapshot(locker, + nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN, + false /* aClearSubsession */, + includeGPUProcess, + processHistArray, + true /* aSkipEmpty */))) { + return NS_ERROR_FAILURE; + } + } + + // Serialize the keyed histograms for every process. + for (uint32_t process = 0; process < processHistArray.length(); ++process) { + aWriter.StartObjectProperty(GetNameForProcessID(ProcessID(process))); + + const KeyedHistogramSnapshotsArray& hArray = processHistArray[process]; + for (size_t i = 0; i < hArray.length(); ++i) { + const KeyedHistogramSnapshotInfo& hData = hArray[i]; + HistogramID id = hData.histogramId; + const HistogramInfo& info = gHistogramInfos[id]; + + aWriter.StartObjectProperty(info.name()); + + // Each key is a new object with a "sum" and a "counts" property. + for (auto iter = hData.data.ConstIter(); !iter.Done(); iter.Next()) { + HistogramSnapshotData& keyData = iter.Data(); + aWriter.StartObjectProperty(PromiseFlatCString(iter.Key()).get()); + internal_ReflectHistogramToJSON(keyData, aWriter); + aWriter.EndObject(); + } + + aWriter.EndObject(); + } + aWriter.EndObject(); + } + + return NS_OK; +} + +nsresult +TelemetryHistogram::DeserializeHistograms(JSContext* aCx, JS::HandleValue aData) +{ + MOZ_ASSERT(XRE_IsParentProcess(), "Only load histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Telemetry is disabled. This should never happen, but let's leave this check + // for consistency with other histogram updates routines. + if (!internal_CanRecordBase()) { + return NS_OK; + } + + typedef mozilla::Tuple<nsCString, nsTArray<Histogram::Count>, int64_t> + PersistedHistogramTuple; + typedef mozilla::Vector<PersistedHistogramTuple> PersistedHistogramArray; + typedef mozilla::Vector<PersistedHistogramArray> PersistedHistogramStorage; + + // Before updating the histograms, we need to get the data out of the JS + // wrappers. We can't hold the histogram mutex while handling JS stuff. + // Build a <histogram name, value> map. + JS::RootedObject histogramDataObj(aCx, &aData.toObject()); + JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, histogramDataObj, &processes)) { + // We can't even enumerate the processes in the loaded data, so + // there is nothing we could recover from the persistence file. Bail out. + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Make sure we have enough storage for all the processes. + PersistedHistogramStorage histogramsToUpdate; + if (!histogramsToUpdate.resize(processes.length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The following block of code attempts to extract as much data as possible + // from the serialized JSON, even in case of light data corruptions: if, for example, + // the data for a single process is corrupted or is in an unexpected form, we press on + // and attempt to load the data for the other processes. + JS::RootedId process(aCx); + for (auto& processVal : processes) { + // This is required as JS API calls require an Handle<jsid> and not a + // plain jsid. + process = processVal; + // Get the process name. + nsAutoJSString processNameJS; + if (!processNameJS.init(aCx, process)) { + JS_ClearPendingException(aCx); + continue; + } + + // Make sure it's valid. Note that this is safe to call outside + // of a locked section. + NS_ConvertUTF16toUTF8 processName(processNameJS); + ProcessID processID = GetIDForProcessName(processName.get()); + if (processID == ProcessID::Count) { + NS_WARNING(nsPrintfCString("Failed to get process ID for %s", processName.get()).get()); + continue; + } + + // And its probes. + JS::RootedValue processData(aCx); + if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) { + JS_ClearPendingException(aCx); + continue; + } + + if (!processData.isObject()) { + // |processData| should be an object containing histograms. If this is + // not the case, silently skip and try to load the data for the other + // processes. + continue; + } + + // Iterate through each histogram. + JS::RootedObject processDataObj(aCx, &processData.toObject()); + JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, processDataObj, &histograms)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get a reference to the deserialized data for this process. + PersistedHistogramArray& deserializedProcessData = + histogramsToUpdate[static_cast<uint32_t>(processID)]; + + JS::RootedId histogram(aCx); + for (auto& histogramVal : histograms) { + histogram = histogramVal; + + int64_t sum = 0; + nsTArray<Histogram::Count> deserializedCounts; + nsCString histogramName; + if (NS_FAILED(internal_ParseHistogramData(aCx, histogram, processDataObj, + histogramName, deserializedCounts, sum))) { + continue; + } + + // Finally append the deserialized data to the storage. + if (!deserializedProcessData.emplaceBack( + MakeTuple(mozilla::Move(histogramName), mozilla::Move(deserializedCounts), sum))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + + // Update the histogram storage. + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + for (uint32_t process = 0; process < histogramsToUpdate.length(); ++process) { + PersistedHistogramArray& processArray = histogramsToUpdate[process]; + + for (auto& histogramData : processArray) { + // Attempt to get the corresponding ID for the deserialized histogram name. + HistogramID id; + if (NS_FAILED(internal_GetHistogramIdByName(mozilla::Get<0>(histogramData), &id))) { + continue; + } + + ProcessID procID = static_cast<ProcessID>(process); + if (!internal_CanRecordHistogram(id, procID)) { + // We're not allowed to record this, so don't try to restore it. + continue; + } + + // Get the Histogram instance: this will instantiate it if it doesn't exist. + Histogram* h = internal_GetHistogramById(id, procID); + MOZ_ASSERT(h); + + if (!h || internal_IsExpired(h)) { + // Don't restore expired histograms. + continue; + } + + // Make sure that histogram counts have matching sizes. If not, + // |AddSampleSet| will fail and crash. + size_t numCounts = mozilla::Get<1>(histogramData).Length(); + if (h->bucket_count() != numCounts) { + MOZ_ASSERT(false, + "The number of restored buckets does not match with the on in the definition"); + continue; + } + + // Update the data for the histogram. + h->AddSampleSet(base::PersistedSampleSet(mozilla::Move(mozilla::Get<1>(histogramData)), + mozilla::Get<2>(histogramData))); + } + } + } + + return NS_OK; +} + +nsresult +TelemetryHistogram::DeserializeKeyedHistograms(JSContext* aCx, JS::HandleValue aData) +{ + MOZ_ASSERT(XRE_IsParentProcess(), "Only load keyed histograms in the parent process"); + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + // Telemetry is disabled. This should never happen, but let's leave this check + // for consistency with other histogram updates routines. + if (!internal_CanRecordBase()) { + return NS_OK; + } + + typedef mozilla::Tuple<nsCString, nsCString, nsTArray<Histogram::Count>, int64_t> + PersistedKeyedHistogramTuple; + typedef mozilla::Vector<PersistedKeyedHistogramTuple> PersistedKeyedHistogramArray; + typedef mozilla::Vector<PersistedKeyedHistogramArray> PersistedKeyedHistogramStorage; + + // Before updating the histograms, we need to get the data out of the JS + // wrappers. We can't hold the histogram mutex while handling JS stuff. + // Build a <histogram name, value> map. + JS::RootedObject histogramDataObj(aCx, &aData.toObject()); + JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, histogramDataObj, &processes)) { + // We can't even enumerate the processes in the loaded data, so + // there is nothing we could recover from the persistence file. Bail out. + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + + // Make sure we have enough storage for all the processes. + PersistedKeyedHistogramStorage histogramsToUpdate; + if (!histogramsToUpdate.resize(processes.length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The following block of code attempts to extract as much data as possible + // from the serialized JSON, even in case of light data corruptions: if, for example, + // the data for a single process is corrupted or is in an unexpected form, we press on + // and attempt to load the data for the other processes. + JS::RootedId process(aCx); + for (auto& processVal : processes) { + // This is required as JS API calls require an Handle<jsid> and not a + // plain jsid. + process = processVal; + // Get the process name. + nsAutoJSString processNameJS; + if (!processNameJS.init(aCx, process)) { + JS_ClearPendingException(aCx); + continue; + } + + // Make sure it's valid. Note that this is safe to call outside + // of a locked section. + NS_ConvertUTF16toUTF8 processName(processNameJS); + ProcessID processID = GetIDForProcessName(processName.get()); + if (processID == ProcessID::Count) { + NS_WARNING(nsPrintfCString("Failed to get process ID for %s", processName.get()).get()); + continue; + } + + // And its probes. + JS::RootedValue processData(aCx); + if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) { + JS_ClearPendingException(aCx); + continue; + } + + if (!processData.isObject()) { + // |processData| should be an object containing histograms. If this is + // not the case, silently skip and try to load the data for the other + // processes. + continue; + } + + // Iterate through each keyed histogram. + JS::RootedObject processDataObj(aCx, &processData.toObject()); + JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, processDataObj, &histograms)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get a reference to the deserialized data for this process. + PersistedKeyedHistogramArray& deserializedProcessData = + histogramsToUpdate[static_cast<uint32_t>(processID)]; + + JS::RootedId histogram(aCx); + for (auto& histogramVal : histograms) { + histogram = histogramVal; + // Get the histogram name. + nsAutoJSString histogramName; + if (!histogramName.init(aCx, histogram)) { + JS_ClearPendingException(aCx); + continue; + } + + // Get the data for this histogram. + JS::RootedValue histogramData(aCx); + if (!JS_GetPropertyById(aCx, processDataObj, histogram, &histogramData)) { + JS_ClearPendingException(aCx); + continue; + } + + // Iterate through each key in the histogram. + JS::RootedObject keysDataObj(aCx, &histogramData.toObject()); + JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx)); + if (!JS_Enumerate(aCx, keysDataObj, &keys)) { + JS_ClearPendingException(aCx); + continue; + } + + JS::RootedId key(aCx); + for (auto& keyVal : keys) { + key = keyVal; + + int64_t sum = 0; + nsTArray<Histogram::Count> deserializedCounts; + nsCString keyName; + if (NS_FAILED(internal_ParseHistogramData(aCx, key, keysDataObj, keyName, + deserializedCounts, sum))) { + continue; + } + + // Finally append the deserialized data to the storage. + if (!deserializedProcessData.emplaceBack( + MakeTuple(nsCString(NS_ConvertUTF16toUTF8(histogramName)), mozilla::Move(keyName), + mozilla::Move(deserializedCounts), sum))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + } + + // Update the keyed histogram storage. + { + StaticMutexAutoLock locker(gTelemetryHistogramMutex); + + for (uint32_t process = 0; process < histogramsToUpdate.length(); ++process) { + PersistedKeyedHistogramArray& processArray = histogramsToUpdate[process]; + + for (auto& histogramData : processArray) { + // Attempt to get the corresponding ID for the deserialized histogram name. + HistogramID id; + if (NS_FAILED(internal_GetHistogramIdByName(mozilla::Get<0>(histogramData), &id))) { + continue; + } + + ProcessID procID = static_cast<ProcessID>(process); + if (!internal_CanRecordHistogram(id, procID)) { + // We're not allowed to record this, so don't try to restore it. + continue; + } + + KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, procID); + MOZ_ASSERT(keyed); + + if (!keyed) { + // Don't restore if we don't have a destination storage. + continue; + } + + // Get data for the key we're looking for. + Histogram* h = nullptr; + if (NS_FAILED(keyed->GetHistogram(mozilla::Get<1>(histogramData), &h))) { + continue; + } + MOZ_ASSERT(h); + + if (!h || internal_IsExpired(h)) { + // Don't restore expired histograms. + continue; + } + + // Make sure that histogram counts have matching sizes. If not, + // |AddSampleSet| will fail and crash. + size_t numCounts = mozilla::Get<2>(histogramData).Length(); + if (h->bucket_count() != numCounts) { + MOZ_ASSERT(false, + "The number of restored buckets does not match with the on in the definition"); + continue; + } + + // Update the data for the histogram. + h->AddSampleSet(base::PersistedSampleSet(mozilla::Move(mozilla::Get<2>(histogramData)), + mozilla::Get<3>(histogramData))); + } + } + } + + return NS_OK; +} +#endif // MOZ_TELEMETRY_GECKOVIEW
--- a/toolkit/components/telemetry/TelemetryHistogram.h +++ b/toolkit/components/telemetry/TelemetryHistogram.h @@ -7,16 +7,23 @@ #define TelemetryHistogram_h__ #include "mozilla/TelemetryHistogramEnums.h" #include "mozilla/TelemetryProcessEnums.h" #include "mozilla/TelemetryComms.h" #include "nsXULAppAPI.h" +#if defined(MOZ_TELEMETRY_GECKOVIEW) +namespace mozilla{ +// This is only used for the GeckoView persistence. +class JSONWriter; +} +#endif + // This module is internal to Telemetry. It encapsulates Telemetry's // histogram accumulation and storage logic. It should only be used by // Telemetry.cpp. These functions should not be used anywhere else. // For the public interface to Telemetry functionality, see Telemetry.h. namespace TelemetryHistogram { void InitializeGlobalState(bool canRecordBase, bool canRecordExtended); @@ -72,11 +79,21 @@ GetKeyedHistogramSnapshots(JSContext *aC bool aClearSubsession); size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf); size_t GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf aMallocSizeOf); +// These functions are only meant to be used for GeckoView persistence. +// They are responsible for updating in-memory probes with the data persisted +// on the disk and vice-versa. +#if defined(MOZ_TELEMETRY_GECKOVIEW) +nsresult SerializeHistograms(mozilla::JSONWriter &aWriter); +nsresult SerializeKeyedHistograms(mozilla::JSONWriter &aWriter); +nsresult DeserializeHistograms(JSContext* aCx, JS::HandleValue aData); +nsresult DeserializeKeyedHistograms(JSContext* aCx, JS::HandleValue aData); +#endif // MOZ_TELEMETRY_GECKOVIEW + } // namespace TelemetryHistogram #endif // TelemetryHistogram_h__
--- a/toolkit/components/telemetry/geckoview/TelemetryGeckoViewPersistence.cpp +++ b/toolkit/components/telemetry/geckoview/TelemetryGeckoViewPersistence.cpp @@ -2,16 +2,17 @@ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TelemetryGeckoViewPersistence.h" #include "jsapi.h" +#include "mozilla/ErrorNames.h" #include "mozilla/JSONWriter.h" #include "mozilla/Path.h" #include "mozilla/Preferences.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPtr.h" #include "mozilla/SystemGroup.h" #include "mozilla/dom/ScriptSettings.h" // for AutoJSAPI #include "mozilla/dom/SimpleGlobalObject.h" @@ -23,18 +24,20 @@ #include "nsISafeOutputStream.h" #include "nsITimer.h" #include "nsLocalFile.h" #include "nsNetUtil.h" #include "nsXULAppAPI.h" #include "prenv.h" #include "prio.h" #include "TelemetryScalar.h" +#include "TelemetryHistogram.h" #include "xpcpublic.h" +using mozilla::GetErrorName; using mozilla::MakeScopeExit; using mozilla::Preferences; using mozilla::StaticRefPtr; using mozilla::SystemGroup; using mozilla::TaskCategory; using mozilla::dom::AutoJSAPI; using mozilla::dom::SimpleGlobalObject; @@ -294,16 +297,57 @@ MainThreadParsePersistedProbes(const nsA ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'keyedScalars'."); MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); } } else { // Getting the "keyedScalars" property failed, suppress the exception // and continue. JS_ClearPendingException(jsapi.cx()); } + + // Get the data for the histograms. + JS::RootedValue histogramData(jsapi.cx()); + if (JS_GetProperty(jsapi.cx(), dataObj, "histograms", &histogramData)) { + // If the data is an object, try to parse its properties. If not, + // silently skip and try to load the other sections. + nsresult rv = NS_OK; + if (!histogramData.isObject() + || NS_FAILED(rv = TelemetryHistogram::DeserializeHistograms(jsapi.cx(), histogramData))) { + nsAutoCString errorName; + GetErrorName(rv, errorName); + ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'histograms', %s.", + errorName.get()); + MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); + } + } else { + // Getting the "histogramData" property failed, suppress the exception + // and continue. + JS_ClearPendingException(jsapi.cx()); + } + + // Get the data for the keyed histograms. + JS::RootedValue keyedHistogramData(jsapi.cx()); + if (JS_GetProperty(jsapi.cx(), dataObj, "keyedHistograms", &keyedHistogramData)) { + // If the data is an object, try to parse its properties. If not, + // silently skip and try to load the other sections. + nsresult rv = NS_OK; + if (!keyedHistogramData.isObject() + || NS_FAILED(rv = TelemetryHistogram::DeserializeKeyedHistograms(jsapi.cx(), + keyedHistogramData))) { + nsAutoCString errorName; + GetErrorName(rv, errorName); + ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'keyedHistograms', %s.", + errorName.get()); + MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); + } + } else { + // Getting the "keyedHistogramData" property failed, suppress the exception + // and continue. + JS_ClearPendingException(jsapi.cx()); + } } /** * The persistence worker function, meant to be run off the main thread. */ void PersistenceThreadPersist() { @@ -345,16 +389,28 @@ PersistenceThreadPersist() w.EndObject(); w.StartObjectProperty("keyedScalars"); if (NS_FAILED(TelemetryScalar::SerializeKeyedScalars(w))) { ANDROID_LOG("Persist - Failed to persist keyed scalars."); } w.EndObject(); + w.StartObjectProperty("histograms"); + if (NS_FAILED(TelemetryHistogram::SerializeHistograms(w))) { + ANDROID_LOG("Persist - Failed to persist histograms."); + } + w.EndObject(); + + w.StartObjectProperty("keyedHistograms"); + if (NS_FAILED(TelemetryHistogram::SerializeKeyedHistograms(w))) { + ANDROID_LOG("Persist - Failed to persist keyed histograms."); + } + w.EndObject(); + // End the building process. w.End(); // Android can kill us while we are writing to disk and, if that happens, // we end up with a corrupted json overwriting the old session data. // Luckily, |StreamingJSONWriter::Close| is smart enough to write to a // temporary file and only overwrite the original file if nothing bad happened. nsresult rv = static_cast<StreamingJSONWriter*>(w.WriteFunc())->Close();
--- a/toolkit/components/telemetry/geckoview/gtest/TestGeckoView.cpp +++ b/toolkit/components/telemetry/geckoview/gtest/TestGeckoView.cpp @@ -27,16 +27,38 @@ const char kSampleData[] = R"({ } }, "keyedScalars": { "parent": { "telemetry.test.keyed_unsigned_int": { "testKey": 73 } } + }, + "histograms": { + "parent": { + "TELEMETRY_TEST_MULTIPRODUCT": { + "sum": 6, + "counts": [ + 3, 5, 7 + ] + } + } + }, + "keyedHistograms": { + "content": { + "TELEMETRY_TEST_MULTIPRODUCT_KEYED": { + "niceKey": { + "sum": 10, + "counts": [ + 1, 2, 3 + ] + } + } + } } })"; const char16_t kPersistedFilename[] = u"gv_measurements.json"; namespace { /** @@ -220,16 +242,90 @@ TestDeserializePersistedKeyedScalars(JSC JS::RootedObject sampleObj(aCx, &sampleData.toObject()); JS::RootedValue keyedScalarData(aCx); ASSERT_TRUE(JS_GetProperty(aCx, sampleObj, "keyedScalars", &keyedScalarData) && keyedScalarData.isObject()) << "Failed to get sampleData['keyedScalars']"; CheckJSONEqual(aCx, aData, keyedScalarData); } +void +TestSerializeHistograms(JSONWriter& aWriter) +{ + // Report the same data that's in kSampleData for histograms. + // We only want to make sure that I/O and parsing works, as telemetry + // measurement updates is taken care of by xpcshell tests. + aWriter.StartObjectProperty("parent"); + aWriter.StartObjectProperty("TELEMETRY_TEST_MULTIPRODUCT"); + aWriter.IntProperty("sum", 6); + aWriter.StartArrayProperty("counts"); + aWriter.IntElement(3); + aWriter.IntElement(5); + aWriter.IntElement(7); + aWriter.EndArray(); + aWriter.EndObject(); + aWriter.EndObject(); +} + +void +TestSerializeKeyedHistograms(JSONWriter& aWriter) +{ + // Report the same data that's in kSampleData for keyed histograms. + // We only want to make sure that I/O and parsing works, as telemetry + // measurement updates is taken care of by xpcshell tests. + aWriter.StartObjectProperty("content"); + aWriter.StartObjectProperty("TELEMETRY_TEST_MULTIPRODUCT_KEYED"); + aWriter.StartObjectProperty("niceKey"); + aWriter.IntProperty("sum", 10); + aWriter.StartArrayProperty("counts"); + aWriter.IntElement(1); + aWriter.IntElement(2); + aWriter.IntElement(3); + aWriter.EndArray(); + aWriter.EndObject(); + aWriter.EndObject(); + aWriter.EndObject(); +} + +void +TestDeserializeHistograms(JSContext* aCx, JS::HandleValue aData) +{ + // Get a JS object out of the JSON sample. + JS::RootedValue sampleData(aCx); + NS_ConvertUTF8toUTF16 utf16Content(kSampleData); + ASSERT_TRUE(JS_ParseJSON(aCx, utf16Content.BeginReading(), utf16Content.Length(), &sampleData)) + << "Failed to create a JS object from the JSON sample"; + + // Get sampleData["histograms"]. + JS::RootedObject sampleObj(aCx, &sampleData.toObject()); + JS::RootedValue histogramData(aCx); + ASSERT_TRUE(JS_GetProperty(aCx, sampleObj, "histograms", &histogramData) && histogramData.isObject()) + << "Failed to get sampleData['histograms']"; + + CheckJSONEqual(aCx, aData, histogramData); +} + +void +TestDeserializeKeyedHistograms(JSContext* aCx, JS::HandleValue aData) +{ + // Get a JS object out of the JSON sample. + JS::RootedValue sampleData(aCx); + NS_ConvertUTF8toUTF16 utf16Content(kSampleData); + ASSERT_TRUE(JS_ParseJSON(aCx, utf16Content.BeginReading(), utf16Content.Length(), &sampleData)) + << "Failed to create a JS object from the JSON sample"; + + // Get sampleData["keyedHistograms"]. + JS::RootedObject sampleObj(aCx, &sampleData.toObject()); + JS::RootedValue keyedHistogramData(aCx); + ASSERT_TRUE(JS_GetProperty(aCx, sampleObj, "keyedHistograms", &keyedHistogramData) + && keyedHistogramData.isObject()) << "Failed to get sampleData['keyedHistograms']"; + + CheckJSONEqual(aCx, aData, keyedHistogramData); +} + } // Anonymous /** * A GeckoView specific test fixture. Please note that this * can't live in the above anonymous namespace. */ class TelemetryGeckoViewFixture : public TelemetryTestFixture { protected: @@ -247,16 +343,25 @@ namespace TelemetryScalar { nsresult SerializeScalars(JSONWriter& aWriter) { TestSerializeScalars(aWriter); return NS_OK; } nsresult SerializeKeyedScalars(JSONWriter& aWriter) { TestSerializeKeyedScalars(aWriter); return NS_OK; } nsresult DeserializePersistedScalars(JSContext* aCx, JS::HandleValue aData) { TestDeserializePersistedScalars(aCx, aData); return NS_OK; } nsresult DeserializePersistedKeyedScalars(JSContext* aCx, JS::HandleValue aData) { TestDeserializePersistedKeyedScalars(aCx, aData); return NS_OK; } } // TelemetryScalar +namespace TelemetryHistogram { + +nsresult SerializeHistograms(mozilla::JSONWriter &aWriter) { TestSerializeHistograms(aWriter); return NS_OK; } +nsresult SerializeKeyedHistograms(mozilla::JSONWriter &aWriter) { TestSerializeKeyedHistograms(aWriter); return NS_OK; } +nsresult DeserializeHistograms(JSContext* aCx, JS::HandleValue aData) { TestDeserializeHistograms(aCx, aData); return NS_OK; } +nsresult DeserializeKeyedHistograms(JSContext* aCx, JS::HandleValue aData) { TestDeserializeKeyedHistograms(aCx, aData); return NS_OK; } + +} // TelemetryHistogram + namespace TelemetryGeckoViewTesting { void TestDispatchPersist(); } // TelemetryGeckoViewTesting /** * Test that corrupted JSON files don't crash the Telemetry core.
--- a/toolkit/components/telemetry/geckoview/gtest/moz.build +++ b/toolkit/components/telemetry/geckoview/gtest/moz.build @@ -17,9 +17,14 @@ LOCAL_INCLUDES += [ DEFINES['MOZ_TELEMETRY_GECKOVIEW'] = True UNIFIED_SOURCES = [ '../TelemetryGeckoViewPersistence.cpp', 'TestGeckoView.cpp', ] +# We need the following line otherwise including +# "TelemetryHistogram.h" in tests will fail due to +# missing headers. +include('/ipc/chromium/chromium-config.mozbuild') + FINAL_LIBRARY = 'xul-gtest'