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,
});
}