tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;