324 lines
9.8 KiB
JavaScript
324 lines
9.8 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
Component,
|
|
createFactory,
|
|
} = require("resource://devtools/client/shared/vendor/react.mjs");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
|
|
|
|
const l10n = new LocalizationHelper(
|
|
"devtools/client/locales/components.properties"
|
|
);
|
|
const dbgL10n = new LocalizationHelper(
|
|
"devtools/client/locales/debugger.properties"
|
|
);
|
|
const Frames = createFactory(
|
|
require("resource://devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.js")
|
|
.Frames
|
|
);
|
|
const {
|
|
annotateFramesWithLibrary,
|
|
} = require("resource://devtools/client/debugger/src/utils/pause/frames/annotateFrames.js");
|
|
const {
|
|
getDisplayURL,
|
|
} = require("resource://devtools/client/debugger/src/utils/sources-tree/getURL.js");
|
|
const {
|
|
getFormattedSourceId,
|
|
} = require("resource://devtools/client/debugger/src/utils/source.js");
|
|
|
|
class SmartTrace extends Component {
|
|
static get propTypes() {
|
|
return {
|
|
stacktrace: PropTypes.array.isRequired,
|
|
onViewSource: PropTypes.func.isRequired,
|
|
onViewSourceInDebugger: PropTypes.func.isRequired,
|
|
// Service to enable the source map feature.
|
|
sourceMapURLService: PropTypes.object,
|
|
// A number in ms (defaults to 100) which we'll wait before doing the first actual
|
|
// render of this component, in order to avoid shifting layout rapidly in case the
|
|
// page is using sourcemap.
|
|
// Setting it to 0 or anything else than a number will force the first render to
|
|
// happen immediatly, without any delay.
|
|
initialRenderDelay: PropTypes.number,
|
|
onSourceMapResultDebounceDelay: PropTypes.number,
|
|
// Function that will be called when the SmartTrace is ready, i.e. once it was
|
|
// rendered.
|
|
onReady: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
static get defaultProps() {
|
|
return {
|
|
initialRenderDelay: 100,
|
|
onSourceMapResultDebounceDelay: 200,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
hasError: false,
|
|
// If a sourcemap service is passed, we want to introduce a small delay in rendering
|
|
// so we can have the results from the sourcemap service, or render if they're not
|
|
// available yet.
|
|
ready: !props.sourceMapURLService || !this.hasInitialRenderDelay(),
|
|
updateCount: 0,
|
|
// Original positions for each indexed position
|
|
originalLocations: null,
|
|
};
|
|
}
|
|
|
|
getChildContext() {
|
|
return { l10n: dbgL10n };
|
|
}
|
|
|
|
// FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
|
|
UNSAFE_componentWillMount() {
|
|
if (this.props.sourceMapURLService) {
|
|
this.sourceMapURLServiceUnsubscriptions = [];
|
|
const sourceMapInit = Promise.all(
|
|
this.props.stacktrace.map(
|
|
({ filename, sourceId, lineNumber, columnNumber }, index) =>
|
|
new Promise(resolve => {
|
|
const callback = originalLocation => {
|
|
this.onSourceMapServiceChange(originalLocation, index);
|
|
resolve();
|
|
};
|
|
|
|
this.sourceMapURLServiceUnsubscriptions.push(
|
|
this.props.sourceMapURLService.subscribeByLocation(
|
|
{
|
|
id: sourceId,
|
|
url: filename.split(" -> ").pop(),
|
|
line: lineNumber,
|
|
column: columnNumber,
|
|
},
|
|
callback
|
|
)
|
|
);
|
|
})
|
|
)
|
|
);
|
|
|
|
// Without initial render delay, we don't have to do anything; if the frames are
|
|
// sourcemapped, we will get new renders from onSourceMapServiceChange.
|
|
if (!this.hasInitialRenderDelay()) {
|
|
return;
|
|
}
|
|
|
|
const delay = new Promise(res => {
|
|
this.initialRenderDelayTimeoutId = setTimeout(
|
|
res,
|
|
this.props.initialRenderDelay
|
|
);
|
|
});
|
|
|
|
// We wait either for the delay to be over (if it exists), or the sourcemapService
|
|
// results to be available, before setting the state as initialized.
|
|
Promise.race([delay, sourceMapInit]).then(() => {
|
|
if (this.initialRenderDelayTimeoutId) {
|
|
clearTimeout(this.initialRenderDelayTimeoutId);
|
|
}
|
|
this.setState(state => ({
|
|
// Force-update so that the ready state is detected.
|
|
updateCount: state.updateCount + 1,
|
|
ready: true,
|
|
}));
|
|
});
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.onReady && this.state.ready) {
|
|
this.props.onReady();
|
|
}
|
|
}
|
|
|
|
shouldComponentUpdate(_, nextState) {
|
|
if (this.state.updateCount !== nextState.updateCount) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
componentDidUpdate(_, previousState) {
|
|
if (this.props.onReady && !previousState.ready && this.state.ready) {
|
|
this.props.onReady();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.initialRenderDelayTimeoutId) {
|
|
clearTimeout(this.initialRenderDelayTimeoutId);
|
|
}
|
|
|
|
if (this.onFrameLocationChangedTimeoutId) {
|
|
clearTimeout(this.initialRenderDelayTimeoutId);
|
|
}
|
|
|
|
if (this.sourceMapURLServiceUnsubscriptions) {
|
|
this.sourceMapURLServiceUnsubscriptions.forEach(unsubscribe => {
|
|
unsubscribe();
|
|
});
|
|
}
|
|
}
|
|
|
|
componentDidCatch(error, info) {
|
|
console.error(
|
|
"Error while rendering stacktrace:",
|
|
error,
|
|
info,
|
|
"props:",
|
|
this.props
|
|
);
|
|
this.setState(state => ({
|
|
// Force-update so the error is detected.
|
|
updateCount: state.updateCount + 1,
|
|
hasError: true,
|
|
}));
|
|
}
|
|
|
|
onSourceMapServiceChange(originalLocation, index) {
|
|
this.setState(({ originalLocations }) => {
|
|
if (!originalLocations) {
|
|
originalLocations = Array.from({
|
|
length: this.props.stacktrace.length,
|
|
});
|
|
}
|
|
return {
|
|
originalLocations: [
|
|
...originalLocations.slice(0, index),
|
|
originalLocation,
|
|
...originalLocations.slice(index + 1),
|
|
],
|
|
};
|
|
});
|
|
|
|
if (this.onFrameLocationChangedTimeoutId) {
|
|
clearTimeout(this.onFrameLocationChangedTimeoutId);
|
|
}
|
|
|
|
// Since a trace may have many original positions, we don't want to
|
|
// constantly re-render every time one becomes available. To avoid this,
|
|
// we only update the component after an initial timeout, and on a
|
|
// debounce edge as more positions load after that.
|
|
if (this.state.ready === true) {
|
|
this.onFrameLocationChangedTimeoutId = setTimeout(() => {
|
|
this.setState(state => ({
|
|
updateCount: state.updateCount + 1,
|
|
}));
|
|
}, this.props.onSourceMapResultDebounceDelay);
|
|
}
|
|
}
|
|
|
|
hasInitialRenderDelay() {
|
|
return (
|
|
Number.isFinite(this.props.initialRenderDelay) &&
|
|
this.props.initialRenderDelay > 0
|
|
);
|
|
}
|
|
|
|
render() {
|
|
if (
|
|
this.state.hasError ||
|
|
(this.hasInitialRenderDelay() && !this.state.ready)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const { onViewSourceInDebugger, onViewSource, stacktrace } = this.props;
|
|
const { originalLocations } = this.state;
|
|
|
|
const frames = stacktrace.map(
|
|
(
|
|
{
|
|
filename,
|
|
sourceId,
|
|
lineNumber,
|
|
columnNumber,
|
|
functionName,
|
|
asyncCause,
|
|
},
|
|
i
|
|
) => {
|
|
// Create partial debugger frontend "location" objects compliant with <Frames> react component requirements
|
|
const sourceUrl = filename.split(" -> ").pop();
|
|
const generatedLocation = {
|
|
line: lineNumber,
|
|
column: columnNumber,
|
|
source: {
|
|
// 'id' isn't used by Frames, but by selectFrame callback below
|
|
id: sourceId,
|
|
url: sourceUrl,
|
|
// Used by FrameComponent
|
|
shortName: sourceUrl
|
|
? getDisplayURL(sourceUrl).filename
|
|
: getFormattedSourceId(sourceId),
|
|
},
|
|
};
|
|
let location = generatedLocation;
|
|
const originalLocation = originalLocations?.[i];
|
|
if (originalLocation) {
|
|
location = {
|
|
line: originalLocation.line,
|
|
column: originalLocation.column,
|
|
source: {
|
|
url: originalLocation.url,
|
|
// Used by FrameComponent
|
|
shortName: getDisplayURL(originalLocation.url).filename,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Create partial debugger frontend "frame" objects compliant with <Frames> react component requirements
|
|
return {
|
|
id: "fake-frame-id-" + i,
|
|
displayName: functionName,
|
|
asyncCause,
|
|
location,
|
|
// Note that for now, Frames component only uses 'location' attribute
|
|
// and never the 'generatedLocation'.
|
|
// But the code below does, the selectFrame callback.
|
|
generatedLocation,
|
|
};
|
|
}
|
|
);
|
|
annotateFramesWithLibrary(frames);
|
|
|
|
return Frames({
|
|
frames,
|
|
selectFrame: ({ generatedLocation }) => {
|
|
const viewSource = onViewSourceInDebugger || onViewSource;
|
|
|
|
viewSource({
|
|
id: generatedLocation.source.id,
|
|
url: generatedLocation.source.url,
|
|
line: generatedLocation.line,
|
|
column: generatedLocation.column,
|
|
});
|
|
},
|
|
getFrameTitle: url => {
|
|
return l10n.getFormatStr("frame.viewsourceindebugger", url);
|
|
},
|
|
disableFrameTruncate: true,
|
|
disableContextMenu: true,
|
|
frameworkGroupingOn: true,
|
|
// Force displaying the original location (we might try to use current Debugger state?)
|
|
shouldDisplayOriginalLocation: true,
|
|
displayFullUrl: !this.state || !this.state.originalLocations,
|
|
panel: "webconsole",
|
|
});
|
|
}
|
|
}
|
|
|
|
SmartTrace.childContextTypes = {
|
|
l10n: PropTypes.object,
|
|
};
|
|
|
|
module.exports = SmartTrace;
|