/* 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/. */ import { actionCreators as ac, actionTypes as at, } from "common/Actions.sys.mjs"; import { perfService as perfSvc } from "content-src/lib/perf-service"; import React from "react"; // Currently record only a fixed set of sections. This will prevent data // from custom sections from showing up or from topstories. const RECORDED_SECTIONS = ["highlights", "topsites"]; export class ComponentPerfTimer extends React.Component { constructor(props) { super(props); // Just for test dependency injection: this.perfSvc = this.props.perfSvc || perfSvc; this._sendBadStateEvent = this._sendBadStateEvent.bind(this); this._sendPaintedEvent = this._sendPaintedEvent.bind(this); this._reportMissingData = false; this._timestampHandled = false; this._recordedFirstRender = false; } componentDidMount() { if (!RECORDED_SECTIONS.includes(this.props.id)) { return; } this._maybeSendPaintedEvent(); } componentDidUpdate() { if (!RECORDED_SECTIONS.includes(this.props.id)) { return; } this._maybeSendPaintedEvent(); } /** * Call the given callback after the upcoming frame paints. * * @note Both setTimeout and requestAnimationFrame are throttled when the page * is hidden, so this callback may get called up to a second or so after the * requestAnimationFrame "paint" for hidden tabs. * * Newtabs hidden while loading will presumably be fairly rare (other than * preloaded tabs, which we will be filtering out on the server side), so such * cases should get lost in the noise. * * If we decide that it's important to find out when something that's hidden * has "painted", however, another option is to post a message to this window. * That should happen even faster than setTimeout, and, at least as of this * writing, it's not throttled in hidden windows in Firefox. * * @param {Function} callback * * @returns void */ _afterFramePaint(callback) { requestAnimationFrame(() => setTimeout(callback, 0)); } _maybeSendBadStateEvent() { // Follow up bugs: // https://github.com/mozilla/activity-stream/issues/3691 if (!this.props.initialized) { // Remember to report back when data is available. this._reportMissingData = true; } else if (this._reportMissingData) { this._reportMissingData = false; // Report how long it took for component to become initialized. this._sendBadStateEvent(); } } _maybeSendPaintedEvent() { // If we've already handled a timestamp, don't do it again. if (this._timestampHandled || !this.props.initialized) { return; } // And if we haven't, we're doing so now, so remember that. Even if // something goes wrong in the callback, we can't try again, as we'd be // sending back the wrong data, and we have to do it here, so that other // calls to this method while waiting for the next frame won't also try to // handle it. this._timestampHandled = true; this._afterFramePaint(this._sendPaintedEvent); } /** * Triggered by call to render. Only first call goes through due to * `_recordedFirstRender`. */ _ensureFirstRenderTsRecorded() { // Used as t0 for recording how long component took to initialize. if (!this._recordedFirstRender) { this._recordedFirstRender = true; // topsites_first_render_ts, highlights_first_render_ts. const key = `${this.props.id}_first_render_ts`; this.perfSvc.mark(key); } } /** * Creates `SAVE_SESSION_PERF_DATA` with timestamp in ms * of how much longer the data took to be ready for display than it would * have been the ideal case. * https://github.com/mozilla/ping-centre/issues/98 */ _sendBadStateEvent() { // highlights_data_ready_ts, topsites_data_ready_ts. const dataReadyKey = `${this.props.id}_data_ready_ts`; this.perfSvc.mark(dataReadyKey); try { const firstRenderKey = `${this.props.id}_first_render_ts`; // value has to be Int32. const value = parseInt( this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), 10 ); this.props.dispatch( ac.OnlyToMain({ type: at.SAVE_SESSION_PERF_DATA, // highlights_data_late_by_ms, topsites_data_late_by_ms. data: { [`${this.props.id}_data_late_by_ms`]: value }, }) ); } catch (ex) { // If this failed, it's likely because the `privacy.resistFingerprinting` // pref is true. } } _sendPaintedEvent() { // Record first_painted event but only send if topsites. if (this.props.id !== "topsites") { return; } // topsites_first_painted_ts. const key = `${this.props.id}_first_painted_ts`; this.perfSvc.mark(key); try { const data = {}; data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key); this.props.dispatch( ac.OnlyToMain({ type: at.SAVE_SESSION_PERF_DATA, data, }) ); } catch (ex) { // If this failed, it's likely because the `privacy.resistFingerprinting` // pref is true. We should at least not blow up, and should continue // to set this._timestampHandled to avoid going through this again. } } render() { if (RECORDED_SECTIONS.includes(this.props.id)) { this._ensureFirstRenderTsRecorded(); this._maybeSendBadStateEvent(); } return this.props.children; } }