Global Metrics

path: .metrics.mi.mi_original
old: -4.263776533558612
new: -4.276936680603782

path: .metrics.mi.mi_sei
old: -41.05630322874228
new: -41.07528930762171

path: .metrics.halstead.time
old: 33791.48815755054
new: 33577.188523141034

path: .metrics.halstead.N2
old: 415.0
new: 416.0

path: .metrics.halstead.vocabulary
old: 114.0
new: 115.0

path: .metrics.halstead.volume
old: 9921.356300567204
new: 9946.497044022177

path: .metrics.halstead.bugs
old: 2.39294083481127
new: 2.3828130397433887

path: .metrics.halstead.estimated_program_length
old: 690.6414151117506
new: 698.5517080276778

path: .metrics.halstead.n2
old: 88.0
new: 89.0

path: .metrics.halstead.purity_ratio
old: 0.47564835751497975
new: 0.4807651122007418

path: .metrics.halstead.difficulty
old: 61.30681818181818
new: 60.764044943820224

path: .metrics.halstead.effort
old: 608246.7868359098
new: 604389.3934165386

path: .metrics.halstead.length
old: 1452.0
new: 1453.0

path: .metrics.halstead.level
old: 0.016311399443929564
new: 0.016457100591715977

Spaces Data

Minimal test - lines (28, 611)

path: .spaces[0].metrics.halstead.vocabulary
old: 104.0
new: 105.0

path: .spaces[0].metrics.halstead.N2
old: 398.0
new: 399.0

path: .spaces[0].metrics.halstead.bugs
old: 2.4410775186906117
new: 2.428963586547769

path: .spaces[0].metrics.halstead.difficulty
old: 66.33333333333333
new: 65.65822784810126

path: .spaces[0].metrics.halstead.volume
old: 9447.62000257894
new: 9473.800425426898

path: .spaces[0].metrics.halstead.purity_ratio
old: 0.4343778764134211
new: 0.4395535873690004

path: .spaces[0].metrics.halstead.level
old: 0.015075376884422112
new: 0.015230383651436284

path: .spaces[0].metrics.halstead.time
old: 34816.22926876313
new: 34557.385940006556

path: .spaces[0].metrics.halstead.n2
old: 78.0
new: 79.0

path: .spaces[0].metrics.halstead.estimated_program_length
old: 612.4728057429238
new: 620.2101117776596

path: .spaces[0].metrics.halstead.effort
old: 626692.1268377362
new: 622032.946920118

path: .spaces[0].metrics.halstead.length
old: 1410.0
new: 1411.0

path: .spaces[0].metrics.mi.mi_original
old: -3.020690235199595
new: -3.0350800941643996

path: .spaces[0].metrics.mi.mi_sei
old: -39.61767301543961
new: -39.63843319360723

Code

class Connector {
  constructor() {
    // Public methods
    this.connect = this.connect.bind(this);
    this.disconnect = this.disconnect.bind(this);
    this.willNavigate = this.willNavigate.bind(this);
    this.navigate = this.navigate.bind(this);
    this.sendHTTPRequest = this.sendHTTPRequest.bind(this);
    this.triggerActivity = this.triggerActivity.bind(this);
    this.getTabTarget = this.getTabTarget.bind(this);
    this.viewSourceInDebugger = this.viewSourceInDebugger.bind(this);
    this.requestData = this.requestData.bind(this);
    this.getTimingMarker = this.getTimingMarker.bind(this);
    this.updateNetworkThrottling = this.updateNetworkThrottling.bind(this);

    // Internals
    this.getLongString = this.getLongString.bind(this);
    this.onTargetAvailable = this.onTargetAvailable.bind(this);
    this.onResourceAvailable = this.onResourceAvailable.bind(this);
    this.onResourceUpdated = this.onResourceUpdated.bind(this);

    this.networkFront = null;
    this.listenForNetworkEvents = true;
  }

  get currentTarget() {
    return this.toolbox.targetList.targetFront;
  }

  get hasResourceWatcherSupport() {
    return this.toolbox.resourceWatcher.hasResourceWatcherSupport(
      this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT
    );
  }

  get watcherFront() {
    return this.toolbox.resourceWatcher.watcherFront;
  }

  /**
   * Connect to the backend.
   *
   * @param {Object} connection object with e.g. reference to the Toolbox.
   * @param {Object} actions (optional) is used to fire Redux actions to update store.
   * @param {Object} getState (optional) is used to get access to the state.
   */
  async connect(connection, actions, getState) {
    this.actions = actions;
    this.getState = getState;
    this.toolbox = connection.toolbox;

    // The owner object (NetMonitorAPI) received all events.
    this.owner = connection.owner;

    await this.toolbox.targetList.watchTargets(
      [this.toolbox.targetList.TYPES.FRAME],
      this.onTargetAvailable
    );

    await this.toolbox.resourceWatcher.watchResources(
      [this.toolbox.resourceWatcher.TYPES.DOCUMENT_EVENT],
      { onAvailable: this.onResourceAvailable }
    );
  }

  disconnect() {
    // As this function might be called twice, we need to guard if already called.
    if (this._destroyed) {
      return;
    }

    this._destroyed = true;

    this.toolbox.targetList.unwatchTargets(
      [this.toolbox.targetList.TYPES.FRAME],
      this.onTargetAvailable
    );

    this.toolbox.resourceWatcher.unwatchResources(
      [this.toolbox.resourceWatcher.TYPES.DOCUMENT_EVENT],
      { onAvailable: this.onResourceAvailable }
    );

    if (this.actions) {
      this.actions.batchReset();
    }

    this.removeListeners();

    this.currentTarget.off("will-navigate", this.willNavigate);

    this.webConsoleFront = null;
    this.dataProvider = null;
  }

  async pause() {
    this.listenForNetworkEvents = false;
  }

  async resume() {
    this.listenForNetworkEvents = true;
  }

  async onTargetAvailable({ targetFront, isTargetSwitching }) {
    if (!targetFront.isTopLevel) {
      return;
    }

    if (isTargetSwitching) {
      this.willNavigate();
    }

    // Listener for `will-navigate` event is (un)registered outside
    // of the `addListeners` and `removeListeners` methods since
    // these are used to pause/resume the connector.
    // Paused network panel should be automatically resumed when page
    // reload, so `will-navigate` listener needs to be there all the time.
    targetFront.on("will-navigate", this.willNavigate);

    this.webConsoleFront = await this.currentTarget.getFront("console");

    this.dataProvider = new FirefoxDataProvider({
      webConsoleFront: this.webConsoleFront,
      actions: this.actions,
      owner: this.owner,
      resourceWatcher: this.toolbox.resourceWatcher,
    });

    // If this is the first top level target, lets register all the listeners
    if (!isTargetSwitching) {
      await this.addListeners();
    }

    // Initialize Responsive Emulation front for network throttling.
    this.responsiveFront = await this.currentTarget.getFront("responsive");
    if (this.hasResourceWatcherSupport) {
      this.networkFront = await this.watcherFront.getNetworkParentActor();
    }
  }

  async onResourceAvailable(resources) {
    for (const resource of resources) {
      const { TYPES } = this.toolbox.resourceWatcher;

      if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
        this.onDocEvent(resource);
        continue;
      }

      if (!this.listenForNetworkEvents) {
        continue;
      }

      if (resource.resourceType === TYPES.NETWORK_EVENT) {
        this.dataProvider.onNetworkResourceAvailable(resource);
        continue;
      }

      if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
        this.dataProvider.onStackTraceAvailable(resource);
        continue;
      }

      if (resource.resourceType === TYPES.WEBSOCKET) {
        const { wsMessageType } = resource;

        switch (wsMessageType) {
          case "webSocketOpened": {
            this.dataProvider.onWebSocketOpened(
              resource.httpChannelId,
              resource.effectiveURI,
              resource.protocols,
              resource.extensions
            );
            break;
          }
          case "webSocketClosed": {
            this.dataProvider.onWebSocketClosed(
              resource.httpChannelId,
              resource.wasClean,
              resource.code,
              resource.reason
            );
            break;
          }
          case "frameReceived": {
            this.dataProvider.onFrameReceived(
              resource.httpChannelId,
              resource.data
            );
            break;
          }
          case "frameSent": {
            this.dataProvider.onFrameSent(
              resource.httpChannelId,
              resource.data
            );
            break;
          }
        }
        continue;
      }

      if (resource.resourceType === TYPES.SERVER_SENT_EVENT) {
        const { messageType, httpChannelId, data } = resource;
        switch (messageType) {
          case "eventSourceConnectionClosed": {
            this.dataProvider.onEventSourceConnectionClosed(httpChannelId);
            break;
          }
          case "eventReceived": {
            this.dataProvider.onEventReceived(httpChannelId, data);
            break;
          }
        }
      }
    }
  }

  async onResourceUpdated(updates) {
    for (const { resource, update } of updates) {
      if (
        resource.resourceType ===
          this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT &&
        this.listenForNetworkEvents
      ) {
        this.dataProvider.onNetworkResourceUpdated(resource, update);
      }
    }
  }

  async addListeners(ignoreExistingResources = false) {
    const targetResources = [
      this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT,
      this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
    ];
    if (Services.prefs.getBoolPref("devtools.netmonitor.features.webSockets")) {
      targetResources.push(this.toolbox.resourceWatcher.TYPES.WEBSOCKET);
    }

    if (
      Services.prefs.getBoolPref(
        "devtools.netmonitor.features.serverSentEvents"
      )
    ) {
      targetResources.push(
        this.toolbox.resourceWatcher.TYPES.SERVER_SENT_EVENT
      );
    }

    await this.toolbox.resourceWatcher.watchResources(targetResources, {
      onAvailable: this.onResourceAvailable,
      onUpdated: this.onResourceUpdated,
      ignoreExistingResources,
    });
  }

  removeListeners() {
    this.toolbox.resourceWatcher.unwatchResources(
      [
        this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT,
        this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
        this.toolbox.resourceWatcher.TYPES.WEBSOCKET,
        this.toolbox.resourceWatcher.TYPES.SERVER_SENT_EVENT,
      ],
      {
        onAvailable: this.onResourceAvailable,
        onUpdated: this.onResourceUpdated,
      }
    );
  }

  enableActions(enable) {
    this.dataProvider.enableActions(enable);
  }

  willNavigate() {
    if (this.actions) {
      if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
        this.actions.batchReset();
        this.actions.clearRequests();
      } else {
        // If the log is persistent, just clear all accumulated timing markers.
        this.actions.clearTimingMarkers();
      }
    }

    if (this.actions && this.getState) {
      const state = this.getState();
      // Resume is done automatically on page reload/navigation.
      if (!state.requests.recording) {
        this.actions.toggleRecording();
      }

      // Stop any ongoing search.
      if (state.search.ongoingSearch) {
        this.actions.stopOngoingSearch();
      }
    }
  }

  navigate() {
    if (!this.dataProvider.hasPendingRequests()) {
      this.onReloaded();
      return;
    }
    const listener = () => {
      if (this.dataProvider && this.dataProvider.hasPendingRequests()) {
        return;
      }
      if (this.owner) {
        this.owner.off(EVENTS.PAYLOAD_READY, listener);
      }
      // Netmonitor may already be destroyed,
      // so do not try to notify the listeners
      if (this.dataProvider) {
        this.onReloaded();
      }
    };
    if (this.owner) {
      this.owner.on(EVENTS.PAYLOAD_READY, listener);
    }
  }

  onReloaded() {
    const panel = this.toolbox.getPanel("netmonitor");
    if (panel) {
      panel.emit("reloaded");
    }
  }

  /**
   * The "DOMContentLoaded" and "Load" events sent by the console actor.
   *
   * @param {object} resource The DOCUMENT_EVENT resource
   */
  onDocEvent(resource) {
    if (!resource.targetFront.isTopLevel) {
      // Only handle document events for the top level target.
      return;
    }

    if (resource.name === "dom-loading") {
      // Netmonitor does not support dom-loading event yet.
      return;
    }

    if (this.actions) {
      this.actions.addTimingMarker(resource);
    }

    if (resource.name === "dom-complete") {
      this.navigate();
    }

    this.emitForTests(TEST_EVENTS.TIMELINE_EVENT, resource);
  }

  /**
   * Send a HTTP request data payload
   *
   * @param {object} data data payload would like to sent to backend
   */
  async sendHTTPRequest(data) {
    if (this.hasResourceWatcherSupport && this.currentTarget) {
      const networkContentFront = await this.currentTarget.getFront(
        "networkContent"
      );
      const { channelId } = await networkContentFront.sendHTTPRequest(data);
      return { channelId };
    }
    const {
      eventActor: { actor },
    } = await this.webConsoleFront.sendHTTPRequest(data);
    return { actor };
  }

  /**
   * Block future requests matching a filter.
   *
   * @param {object} filter request filter specifying what to block
   */
  blockRequest(filter) {
    return this.webConsoleFront.blockRequest(filter);
  }

  /**
   * Unblock future requests matching a filter.
   *
   * @param {object} filter request filter specifying what to unblock
   */
  unblockRequest(filter) {
    return this.webConsoleFront.unblockRequest(filter);
  }

  /*
   * Get the list of blocked URLs
   */
  async getBlockedUrls() {
    if (this.hasResourceWatcherSupport && this.networkFront) {
      return this.networkFront.getBlockedUrls();
    }
    if (!this.webConsoleFront.traits.blockedUrls) {
      return [];
    }
    return this.webConsoleFront.getBlockedUrls();
  }

  /**
   * Updates the list of blocked URLs
   *
   * @param {object} urls An array of URL strings
   */
  async setBlockedUrls(urls) {
    if (this.hasResourceWatcherSupport && this.networkFront) {
      return this.networkFront.setBlockedUrls(urls);
    }
    return this.webConsoleFront.setBlockedUrls(urls);
  }

  /**
   * Triggers a specific "activity" to be performed by the frontend.
   * This can be, for example, triggering reloads or enabling/disabling cache.
   *
   * @param {number} type The activity type. See the ACTIVITY_TYPE const.
   * @return {object} A promise resolved once the activity finishes and the frontend
   *                  is back into "standby" mode.
   */
  triggerActivity(type) {
    // Puts the frontend into "standby" (when there's no particular activity).
    const standBy = () => {
      this.currentActivity = ACTIVITY_TYPE.NONE;
    };

    // Waits for a series of "navigation start" and "navigation stop" events.
    const waitForNavigation = async () => {
      await this.currentTarget.once("will-navigate");
      await this.currentTarget.once("navigate");
    };

    // Reconfigures the tab, optionally triggering a reload.
    const reconfigureTab = options => {
      return this.toolbox.targetList.updateConfiguration(options);
    };

    // Reconfigures the tab and waits for the target to finish navigating.
    const reconfigureTabAndReload = async options => {
      const navigationFinished = waitForNavigation();
      await reconfigureTab(options);
      await this.toolbox.target.reload();
      await navigationFinished;
    };

    switch (type) {
      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DEFAULT:
        return reconfigureTabAndReload({}).then(standBy);
      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED:
        this.currentActivity = ACTIVITY_TYPE.ENABLE_CACHE;
        this.currentTarget.once("will-navigate", () => {
          this.currentActivity = type;
        });
        return reconfigureTabAndReload({
          cacheDisabled: false,
        }).then(standBy);
      case ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED:
        this.currentActivity = ACTIVITY_TYPE.DISABLE_CACHE;
        this.currentTarget.once("will-navigate", () => {
          this.currentActivity = type;
        });
        return reconfigureTabAndReload({
          cacheDisabled: true,
        }).then(standBy);
      case ACTIVITY_TYPE.ENABLE_CACHE:
        this.currentActivity = type;
        return reconfigureTab({
          cacheDisabled: false,
        }).then(standBy);
      case ACTIVITY_TYPE.DISABLE_CACHE:
        this.currentActivity = type;
        return reconfigureTab({
          cacheDisabled: true,
        }).then(standBy);
    }
    this.currentActivity = ACTIVITY_TYPE.NONE;
    return Promise.reject(new Error("Invalid activity type"));
  }

  /**
   * Fetches the full text of a LongString.
   *
   * @param {object|string} stringGrip
   *        The long string grip containing the corresponding actor.
   *        If you pass in a plain string (by accident or because you're lazy),
   *        then a promise of the same string is simply returned.
   * @return {object}
   *         A promise that is resolved when the full string contents
   *         are available, or rejected if something goes wrong.
   */
  getLongString(stringGrip) {
    return this.dataProvider.getLongString(stringGrip);
  }

  /**
   * Getter that access tab target instance.
   * @return {object} browser tab target instance
   */
  getTabTarget() {
    return this.currentTarget;
  }

  /**
   * Getter that returns the current toolbox instance.
   * @return {Toolbox} toolbox instance
   */
  getToolbox() {
    return this.toolbox;
  }

  /**
   * Open a given source in Debugger
   * @param {string} sourceURL source url
   * @param {number} sourceLine source line number
   */
  viewSourceInDebugger(sourceURL, sourceLine, sourceColumn) {
    if (this.toolbox) {
      this.toolbox.viewSourceInDebugger(sourceURL, sourceLine, sourceColumn);
    }
  }

  /**
   * Fetch networkEventUpdate websocket message from back-end when
   * data provider is connected.
   * @param {object} request network request instance
   * @param {string} type NetworkEventUpdate type
   */
  requestData(request, type) {
    return this.dataProvider.requestData(request, type);
  }

  getTimingMarker(name) {
    if (!this.getState) {
      return -1;
    }

    const state = this.getState();
    return getDisplayedTimingMarker(state, name);
  }

  async updateNetworkThrottling(enabled, profile) {
    const throttlingFront =
      this.hasResourceWatcherSupport && this.networkFront
        ? this.networkFront
        : this.responsiveFront;

    if (!enabled) {
      throttlingFront.clearNetworkThrottling();
    } else {
      // The profile can be either a profile id which is used to
      // search the predefined throttle profiles or a profile object
      // as defined in the trottle tests.
      if (typeof profile === "string") {
        profile = throttlingProfiles.find(({ id }) => id == profile);
      }
      const { download, upload, latency } = profile;
      await throttlingFront.setNetworkThrottling({
        downloadThroughput: download,
        uploadThroughput: upload,
        latency,
      });
    }

    this.emitForTests(TEST_EVENTS.THROTTLING_CHANGED, { profile });
  }

  /**
   * Fire events for the owner object. These events are only
   * used in tests so, don't fire them in production release.
   */
  emitForTests(type, data) {
    if (this.owner) {
      this.owner.emitForTests(type, data);
    }
  }
}

Minimal test - lines (259, 283)

path: .spaces[0].spaces[11].metrics.halstead.purity_ratio
old: 0.7544158142160169
new: 0.8014615591585207

path: .spaces[0].spaces[11].metrics.halstead.N2
old: 16.0
new: 17.0

path: .spaces[0].spaces[11].metrics.halstead.n2
old: 6.0
new: 7.0

path: .spaces[0].spaces[11].metrics.halstead.difficulty
old: 14.666666666666666
new: 13.357142857142858

path: .spaces[0].spaces[11].metrics.halstead.level
old: 0.06818181818181819
new: 0.0748663101604278

path: .spaces[0].spaces[11].metrics.halstead.bugs
old: 0.0875469735391601
new: 0.0841385363353353

path: .spaces[0].spaces[11].metrics.halstead.estimated_program_length
old: 53.5635228093372
new: 57.70523225941349

path: .spaces[0].spaces[11].metrics.halstead.volume
old: 290.20986172877406
new: 300.23460010384645

path: .spaces[0].spaces[11].metrics.halstead.length
old: 71.0
new: 72.0

path: .spaces[0].spaces[11].metrics.halstead.vocabulary
old: 17.0
new: 18.0

path: .spaces[0].spaces[11].metrics.halstead.time
old: 236.46729474196405
new: 222.79313579134637

path: .spaces[0].spaces[11].metrics.halstead.effort
old: 4256.411305355353
new: 4010.276444244235

path: .spaces[0].spaces[11].metrics.mi.mi_original
old: 88.67706916066679
new: 88.50047795625828

path: .spaces[0].spaces[11].metrics.mi.mi_sei
old: 52.538575504862266
new: 52.283808249997506

path: .spaces[0].spaces[11].metrics.mi.mi_visual_studio
old: 51.85793518167649
new: 51.75466547149607

Code

  async addListeners(ignoreExistingResources = false) {
    const targetResources = [
      this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT,
      this.toolbox.resourceWatcher.TYPES.NETWORK_EVENT_STACKTRACE,
    ];
    if (Services.prefs.getBoolPref("devtools.netmonitor.features.webSockets")) {
      targetResources.push(this.toolbox.resourceWatcher.TYPES.WEBSOCKET);
    }

    if (
      Services.prefs.getBoolPref(
        "devtools.netmonitor.features.serverSentEvents"
      )
    ) {
      targetResources.push(
        this.toolbox.resourceWatcher.TYPES.SERVER_SENT_EVENT
      );
    }

    await this.toolbox.resourceWatcher.watchResources(targetResources, {
      onAvailable: this.onResourceAvailable,
      onUpdated: this.onResourceUpdated,
      ignoreExistingResources,
    });
  }