ComponentPerfTimer.jsx (5761B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 6 import { perfService as perfSvc } from "content-src/lib/perf-service"; 7 import React from "react"; 8 9 // Currently record only a fixed set of sections. This will prevent data 10 // from custom sections from showing up or from topstories. 11 const RECORDED_SECTIONS = ["highlights", "topsites"]; 12 13 export class ComponentPerfTimer extends React.Component { 14 constructor(props) { 15 super(props); 16 // Just for test dependency injection: 17 this.perfSvc = this.props.perfSvc || perfSvc; 18 19 this._sendBadStateEvent = this._sendBadStateEvent.bind(this); 20 this._sendPaintedEvent = this._sendPaintedEvent.bind(this); 21 this._reportMissingData = false; 22 this._timestampHandled = false; 23 this._recordedFirstRender = false; 24 } 25 26 componentDidMount() { 27 if (!RECORDED_SECTIONS.includes(this.props.id)) { 28 return; 29 } 30 31 this._maybeSendPaintedEvent(); 32 } 33 34 componentDidUpdate() { 35 if (!RECORDED_SECTIONS.includes(this.props.id)) { 36 return; 37 } 38 39 this._maybeSendPaintedEvent(); 40 } 41 42 /** 43 * Call the given callback after the upcoming frame paints. 44 * 45 * Note: Both setTimeout and requestAnimationFrame are throttled when the page 46 * is hidden, so this callback may get called up to a second or so after the 47 * requestAnimationFrame "paint" for hidden tabs. 48 * 49 * Newtabs hidden while loading will presumably be fairly rare (other than 50 * preloaded tabs, which we will be filtering out on the server side), so such 51 * cases should get lost in the noise. 52 * 53 * If we decide that it's important to find out when something that's hidden 54 * has "painted", however, another option is to post a message to this window. 55 * That should happen even faster than setTimeout, and, at least as of this 56 * writing, it's not throttled in hidden windows in Firefox. 57 * 58 * @param {Function} callback 59 * 60 * @returns void 61 */ 62 _afterFramePaint(callback) { 63 requestAnimationFrame(() => setTimeout(callback, 0)); 64 } 65 66 _maybeSendBadStateEvent() { 67 // Follow up bugs: 68 // https://github.com/mozilla/activity-stream/issues/3691 69 if (!this.props.initialized) { 70 // Remember to report back when data is available. 71 this._reportMissingData = true; 72 } else if (this._reportMissingData) { 73 this._reportMissingData = false; 74 // Report how long it took for component to become initialized. 75 this._sendBadStateEvent(); 76 } 77 } 78 79 _maybeSendPaintedEvent() { 80 // If we've already handled a timestamp, don't do it again. 81 if (this._timestampHandled || !this.props.initialized) { 82 return; 83 } 84 85 // And if we haven't, we're doing so now, so remember that. Even if 86 // something goes wrong in the callback, we can't try again, as we'd be 87 // sending back the wrong data, and we have to do it here, so that other 88 // calls to this method while waiting for the next frame won't also try to 89 // handle it. 90 this._timestampHandled = true; 91 this._afterFramePaint(this._sendPaintedEvent); 92 } 93 94 /** 95 * Triggered by call to render. Only first call goes through due to 96 * `_recordedFirstRender`. 97 */ 98 _ensureFirstRenderTsRecorded() { 99 // Used as t0 for recording how long component took to initialize. 100 if (!this._recordedFirstRender) { 101 this._recordedFirstRender = true; 102 // topsites_first_render_ts, highlights_first_render_ts. 103 const key = `${this.props.id}_first_render_ts`; 104 this.perfSvc.mark(key); 105 } 106 } 107 108 /** 109 * Creates `SAVE_SESSION_PERF_DATA` with timestamp in ms 110 * of how much longer the data took to be ready for display than it would 111 * have been the ideal case. 112 * https://github.com/mozilla/ping-centre/issues/98 113 */ 114 _sendBadStateEvent() { 115 // highlights_data_ready_ts, topsites_data_ready_ts. 116 const dataReadyKey = `${this.props.id}_data_ready_ts`; 117 this.perfSvc.mark(dataReadyKey); 118 119 try { 120 const firstRenderKey = `${this.props.id}_first_render_ts`; 121 // value has to be Int32. 122 const value = parseInt( 123 this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - 124 this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), 125 10 126 ); 127 this.props.dispatch( 128 ac.OnlyToMain({ 129 type: at.SAVE_SESSION_PERF_DATA, 130 // highlights_data_late_by_ms, topsites_data_late_by_ms. 131 data: { [`${this.props.id}_data_late_by_ms`]: value }, 132 }) 133 ); 134 } catch (ex) { 135 // If this failed, it's likely because the `privacy.resistFingerprinting` 136 // pref is true. 137 } 138 } 139 140 _sendPaintedEvent() { 141 // Record first_painted event but only send if topsites. 142 if (this.props.id !== "topsites") { 143 return; 144 } 145 146 // topsites_first_painted_ts. 147 const key = `${this.props.id}_first_painted_ts`; 148 this.perfSvc.mark(key); 149 150 try { 151 const data = {}; 152 data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key); 153 154 this.props.dispatch( 155 ac.OnlyToMain({ 156 type: at.SAVE_SESSION_PERF_DATA, 157 data, 158 }) 159 ); 160 } catch (ex) { 161 // If this failed, it's likely because the `privacy.resistFingerprinting` 162 // pref is true. We should at least not blow up, and should continue 163 // to set this._timestampHandled to avoid going through this again. 164 } 165 } 166 167 render() { 168 if (RECORDED_SECTIONS.includes(this.props.id)) { 169 this._ensureFirstRenderTsRecorded(); 170 this._maybeSendBadStateEvent(); 171 } 172 return this.props.children; 173 } 174 }