SmartTrace.js (10731B)
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 "use strict"; 6 7 const { 8 Component, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 13 14 const l10n = new LocalizationHelper( 15 "devtools/client/locales/components.properties" 16 ); 17 const dbgL10n = new LocalizationHelper( 18 "devtools/client/locales/debugger.properties" 19 ); 20 const Frames = createFactory( 21 require("resource://devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.js") 22 .Frames 23 ); 24 const { 25 annotateFramesWithLibrary, 26 } = require("resource://devtools/client/debugger/src/utils/pause/frames/annotateFrames.js"); 27 const { 28 getDisplayURL, 29 } = require("resource://devtools/client/debugger/src/utils/sources-tree/getURL.js"); 30 const { 31 getFormattedSourceId, 32 } = require("resource://devtools/client/debugger/src/utils/source.js"); 33 34 class SmartTrace extends Component { 35 static get propTypes() { 36 return { 37 stacktrace: PropTypes.array.isRequired, 38 onViewSource: PropTypes.func.isRequired, 39 onViewSourceInDebugger: PropTypes.func.isRequired, 40 // Service to enable the source map feature. 41 sourceMapURLService: PropTypes.object, 42 // A number in ms (defaults to 100) which we'll wait before doing the first actual 43 // render of this component, in order to avoid shifting layout rapidly in case the 44 // page is using sourcemap. 45 // Setting it to 0 or anything else than a number will force the first render to 46 // happen immediatly, without any delay. 47 initialRenderDelay: PropTypes.number, 48 onSourceMapResultDebounceDelay: PropTypes.number, 49 // Function that will be called when the SmartTrace is ready, i.e. once it was 50 // rendered. 51 onReady: PropTypes.func, 52 }; 53 } 54 55 static get defaultProps() { 56 return { 57 initialRenderDelay: 100, 58 onSourceMapResultDebounceDelay: 200, 59 }; 60 } 61 62 constructor(props) { 63 super(props); 64 this.state = { 65 hasError: false, 66 // If a sourcemap service is passed, we want to introduce a small delay in rendering 67 // so we can have the results from the sourcemap service, or render if they're not 68 // available yet. 69 ready: !props.sourceMapURLService || !this.hasInitialRenderDelay(), 70 updateCount: 0, 71 // Original positions for each indexed position 72 originalLocations: null, 73 }; 74 } 75 76 getChildContext() { 77 return { l10n: dbgL10n }; 78 } 79 80 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 81 UNSAFE_componentWillMount() { 82 if (this.props.sourceMapURLService) { 83 this.sourceMapURLServiceUnsubscriptions = []; 84 const sourceMapInit = Promise.all( 85 this.props.stacktrace.map( 86 ({ filename, sourceId, lineNumber, columnNumber }, index) => 87 new Promise(resolve => { 88 const callback = originalLocation => { 89 this.onSourceMapServiceChange(originalLocation, index); 90 resolve(); 91 }; 92 93 this.sourceMapURLServiceUnsubscriptions.push( 94 this.props.sourceMapURLService.subscribeByLocation( 95 { 96 id: sourceId, 97 url: filename.split(" -> ").pop(), 98 line: lineNumber, 99 // stacktrace uses 1-based column whereas SourceMapURLService/SourceMapLoader/SourceMap used 0-based columns 100 column: columnNumber - 1, 101 }, 102 callback 103 ) 104 ); 105 }) 106 ) 107 ); 108 109 // Without initial render delay, we don't have to do anything; if the frames are 110 // sourcemapped, we will get new renders from onSourceMapServiceChange. 111 if (!this.hasInitialRenderDelay()) { 112 return; 113 } 114 115 const delay = new Promise(res => { 116 this.initialRenderDelayTimeoutId = setTimeout( 117 res, 118 this.props.initialRenderDelay 119 ); 120 }); 121 122 // We wait either for the delay to be over (if it exists), or the sourcemapService 123 // results to be available, before setting the state as initialized. 124 Promise.race([delay, sourceMapInit]).then(() => { 125 if (this.initialRenderDelayTimeoutId) { 126 clearTimeout(this.initialRenderDelayTimeoutId); 127 } 128 this.setState(state => ({ 129 // Force-update so that the ready state is detected. 130 updateCount: state.updateCount + 1, 131 ready: true, 132 })); 133 }); 134 } 135 } 136 137 componentDidMount() { 138 if (this.props.onReady && this.state.ready) { 139 this.props.onReady(); 140 } 141 } 142 143 shouldComponentUpdate(_, nextState) { 144 if (this.state.updateCount !== nextState.updateCount) { 145 return true; 146 } 147 148 return false; 149 } 150 151 componentDidUpdate(_, previousState) { 152 if (this.props.onReady && !previousState.ready && this.state.ready) { 153 this.props.onReady(); 154 } 155 } 156 157 componentWillUnmount() { 158 if (this.initialRenderDelayTimeoutId) { 159 clearTimeout(this.initialRenderDelayTimeoutId); 160 } 161 162 if (this.onFrameLocationChangedTimeoutId) { 163 clearTimeout(this.initialRenderDelayTimeoutId); 164 } 165 166 if (this.sourceMapURLServiceUnsubscriptions) { 167 this.sourceMapURLServiceUnsubscriptions.forEach(unsubscribe => { 168 unsubscribe(); 169 }); 170 } 171 } 172 173 componentDidCatch(error, info) { 174 console.error( 175 "Error while rendering stacktrace:", 176 error, 177 info, 178 "props:", 179 this.props 180 ); 181 this.setState(state => ({ 182 // Force-update so the error is detected. 183 updateCount: state.updateCount + 1, 184 hasError: true, 185 })); 186 } 187 188 onSourceMapServiceChange(originalLocation, index) { 189 this.setState(({ originalLocations }) => { 190 if (!originalLocations) { 191 originalLocations = Array.from({ 192 length: this.props.stacktrace.length, 193 }); 194 } 195 return { 196 originalLocations: [ 197 ...originalLocations.slice(0, index), 198 originalLocation, 199 ...originalLocations.slice(index + 1), 200 ], 201 }; 202 }); 203 204 if (this.onFrameLocationChangedTimeoutId) { 205 clearTimeout(this.onFrameLocationChangedTimeoutId); 206 } 207 208 // Since a trace may have many original positions, we don't want to 209 // constantly re-render every time one becomes available. To avoid this, 210 // we only update the component after an initial timeout, and on a 211 // debounce edge as more positions load after that. 212 if (this.state.ready === true) { 213 this.onFrameLocationChangedTimeoutId = setTimeout(() => { 214 this.setState(state => ({ 215 updateCount: state.updateCount + 1, 216 })); 217 }, this.props.onSourceMapResultDebounceDelay); 218 } 219 } 220 221 hasInitialRenderDelay() { 222 return ( 223 Number.isFinite(this.props.initialRenderDelay) && 224 this.props.initialRenderDelay > 0 225 ); 226 } 227 228 render() { 229 if ( 230 this.state.hasError || 231 (this.hasInitialRenderDelay() && !this.state.ready) 232 ) { 233 return null; 234 } 235 236 const { onViewSourceInDebugger, onViewSource, stacktrace } = this.props; 237 const { originalLocations } = this.state; 238 239 // `stacktrace` is either: 240 // - the `preview` attribute of an object actor's grip object for an Error JS Object when used from the ObjectInspector 241 // - the Console/CSS message or page error resource's `stacktrace` attribute 242 // In both bases both lineColumn and columnNumber are 1-based as relating to SavedFrames attributes being 1-based 243 const frames = stacktrace.map( 244 ( 245 { 246 filename, 247 sourceId, 248 lineNumber, 249 columnNumber, 250 functionName, 251 asyncCause, 252 }, 253 i 254 ) => { 255 // Create partial debugger frontend "location" objects compliant with <Frames> react component requirements 256 const sourceUrl = filename.split(" -> ").pop(); 257 const generatedLocation = { 258 // Line is 1-based 259 line: lineNumber, 260 // SavedFrame/stacktrace column are 1-based while the debugger ones are 0-based 261 column: columnNumber - 1, 262 source: { 263 // 'id' isn't used by Frames, but by selectFrame callback below 264 id: sourceId, 265 url: sourceUrl, 266 // Used by FrameComponent 267 shortName: sourceUrl 268 ? getDisplayURL(sourceUrl).filename 269 : getFormattedSourceId(sourceId), 270 }, 271 }; 272 let location = generatedLocation; 273 const originalLocation = originalLocations?.[i]; 274 if (originalLocation) { 275 location = { 276 // Original lines are 1-based 277 line: originalLocation.line, 278 // Original lines are 0-based 279 column: originalLocation.column, 280 source: { 281 url: originalLocation.url, 282 // Used by FrameComponent 283 shortName: getDisplayURL(originalLocation.url).filename, 284 }, 285 }; 286 } 287 288 // Create partial debugger frontend "frame" objects compliant with <Frames> react component requirements 289 return { 290 id: "fake-frame-id-" + i, 291 displayName: functionName, 292 asyncCause, 293 location, 294 // Note that for now, Frames component only uses 'location' attribute 295 // and never the 'generatedLocation'. 296 // But the code below does, the selectFrame callback. 297 generatedLocation, 298 }; 299 } 300 ); 301 annotateFramesWithLibrary(frames); 302 303 return Frames({ 304 frames, 305 selectFrame: ({ generatedLocation }) => { 306 const viewSource = onViewSourceInDebugger || onViewSource; 307 308 viewSource({ 309 id: generatedLocation.source.id, 310 url: generatedLocation.source.url, 311 line: generatedLocation.line, 312 column: generatedLocation.column, 313 }); 314 }, 315 getFrameTitle: url => { 316 return l10n.getFormatStr("frame.viewsourceindebugger", url); 317 }, 318 disableFrameTruncate: true, 319 disableContextMenu: true, 320 frameworkGroupingOn: true, 321 // Force displaying the original location (we might try to use current Debugger state?) 322 shouldDisplayOriginalLocation: true, 323 displayFullUrl: !this.state || !this.state.originalLocations, 324 panel: "webconsole", 325 }); 326 } 327 } 328 329 SmartTrace.childContextTypes = { 330 l10n: PropTypes.object, 331 }; 332 333 module.exports = SmartTrace;