Added a tooltip to the Priority column to show final priority. Added getRequestPriorityAsNumber function to convert HAR priority string back into a value. Update representation of the priority value of a resource when it changes. Added field priority to HAR file export and importing. Linting wants double quotes. priority **needs** to be gathered first before assigning into resource. Check if the priority has changed to decide if Priority should re-render. Added test to check priority of images above and below the fold. Changed test HTML file into a hand written example with some explanation. Guard against null priority when updating it. Use isInteger to check priority is really a number. We do not want to overwrite priority with "undefined". Defer setting up the first test image a little bit, but do not check its priority. Test HTML needs to be large. Differential Revision: https://phabricator.services.mozilla.com/D287020
384 lines
12 KiB
JavaScript
384 lines
12 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";
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"NetworkEventActor",
|
|
"resource://devtools/server/actors/network-monitor/network-event-actor.js",
|
|
true
|
|
);
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(
|
|
lazy,
|
|
{
|
|
NetworkUtils:
|
|
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
|
|
},
|
|
{ global: "contextual" }
|
|
);
|
|
|
|
// Internal resource types used to create the appropriate network event based
|
|
// on where the channel / resource is coming from.
|
|
const RESOURCE_TYPES = {
|
|
BLOCKED: "blocked-resource",
|
|
CACHED: "cached-resource",
|
|
DATA_CHANNEL: "data-channel-resource",
|
|
};
|
|
|
|
/**
|
|
* Handles network events from the content process
|
|
* This currently only handles events for requests (js/css) blocked by CSP.
|
|
*/
|
|
class NetworkEventContentWatcher {
|
|
/**
|
|
* Start watching for all network events related to a given Target Actor.
|
|
*
|
|
* @param TargetActor targetActor
|
|
* The target actor in the content process from which we should
|
|
* observe network events.
|
|
* @param Object options
|
|
* Dictionary object with following attributes:
|
|
* - onAvailable: mandatory function
|
|
* This will be called for each resource.
|
|
* - onUpdated: optional function
|
|
* This would be called multiple times for each resource.
|
|
*/
|
|
async watch(targetActor, { onAvailable, onUpdated }) {
|
|
// Map from channelId to network event objects.
|
|
this.networkEvents = new Map();
|
|
|
|
this.targetActor = targetActor;
|
|
this.onAvailable = onAvailable;
|
|
this.onUpdated = onUpdated;
|
|
|
|
Services.obs.addObserver(
|
|
this.httpFailedOpeningRequest,
|
|
"http-on-failed-opening-request"
|
|
);
|
|
|
|
Services.obs.addObserver(
|
|
this.httpOnResourceCacheResponse,
|
|
"http-on-resource-cache-response"
|
|
);
|
|
|
|
Services.obs.addObserver(this.onDataChannelOpened, "data-channel-opened");
|
|
}
|
|
/**
|
|
* Allows clearing of network events
|
|
*/
|
|
clear() {
|
|
this.networkEvents.clear();
|
|
}
|
|
|
|
httpFailedOpeningRequest = (subject, topic) => {
|
|
if (
|
|
topic != "http-on-failed-opening-request" ||
|
|
!(subject instanceof Ci.nsIHttpChannel)
|
|
) {
|
|
const channel = subject.QueryInterface(Ci.nsIChannel);
|
|
console.warn(
|
|
`httpFailedOpeningRequest triggered on non-nsIHttpChannel for uri: ${channel.URI.spec}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const channel = subject.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
// Ignore preload requests to avoid duplicity request entries in
|
|
// the Network panel. If a preload fails (for whatever reason)
|
|
// then the platform kicks off another 'real' request.
|
|
if (lazy.NetworkUtils.isPreloadRequest(channel)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!lazy.NetworkUtils.matchRequest(channel, {
|
|
targetActor: this.targetActor,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.onNetworkEventAvailable(channel, {
|
|
networkEventOptions: {
|
|
blockedReason: channel.loadInfo.requestBlockingReason,
|
|
},
|
|
type: RESOURCE_TYPES.BLOCKED,
|
|
});
|
|
};
|
|
|
|
httpOnResourceCacheResponse = (subject, topic, memoryCacheKey) => {
|
|
if (
|
|
topic != "http-on-resource-cache-response" ||
|
|
!(subject instanceof Ci.nsIHttpChannel)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const channel = subject.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
if (
|
|
!lazy.NetworkUtils.matchRequest(channel, {
|
|
targetActor: this.targetActor,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
channel.loadInfo?.externalContentPolicyType !==
|
|
Ci.nsIContentPolicy.TYPE_SCRIPT
|
|
) {
|
|
// For images and stylesheets from the cache, only one network request
|
|
// should be created per URI.
|
|
//
|
|
// For scripts from the cache, multiple network requests should be
|
|
// created.
|
|
const hasURI = Array.from(this.networkEvents.values()).some(
|
|
networkEvent => networkEvent.uri === channel.URI.spec
|
|
);
|
|
|
|
if (hasURI) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.onNetworkEventAvailable(channel, {
|
|
fromCache: true,
|
|
memoryCacheKey,
|
|
networkEventOptions: {},
|
|
type: RESOURCE_TYPES.CACHED,
|
|
});
|
|
};
|
|
|
|
onDataChannelOpened = (subject, topic) => {
|
|
if (
|
|
topic != "data-channel-opened" ||
|
|
!(subject instanceof Ci.nsIDataChannel)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const channel = subject.QueryInterface(Ci.nsIDataChannel);
|
|
channel.QueryInterface(Ci.nsIIdentChannel);
|
|
channel.QueryInterface(Ci.nsIChannel);
|
|
|
|
if (channel.isDocument) {
|
|
// Navigation data channels are available in the parent process and will
|
|
// be monitored there.
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!lazy.NetworkUtils.matchRequest(channel, {
|
|
targetActor: this.targetActor,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.onNetworkEventAvailable(channel, {
|
|
fromCache: false,
|
|
networkEventOptions: {},
|
|
type: RESOURCE_TYPES.DATA_CHANNEL,
|
|
});
|
|
};
|
|
|
|
onNetworkEventAvailable(
|
|
channel,
|
|
{ fromCache, memoryCacheKey, networkEventOptions, type }
|
|
) {
|
|
const networkEventActor = new NetworkEventActor(
|
|
this.targetActor.conn,
|
|
this.targetActor.sessionContext,
|
|
{
|
|
onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
|
|
onNetworkEventDestroy: this.onNetworkEventDestroyed.bind(this),
|
|
},
|
|
networkEventOptions,
|
|
channel
|
|
);
|
|
this.targetActor.manage(networkEventActor);
|
|
|
|
const resource = networkEventActor.asResource();
|
|
|
|
const networkEvent = {
|
|
browsingContextID: resource.browsingContextID,
|
|
innerWindowId: resource.innerWindowId,
|
|
resourceId: resource.resourceId,
|
|
receivedUpdates: [],
|
|
resourceUpdates: {},
|
|
uri: channel.URI.spec,
|
|
};
|
|
|
|
// Requests already come with request cookies and headers, so those
|
|
// should always be considered as available. But the client still
|
|
// heavily relies on those `Available` flags to fetch additional data,
|
|
// so it is better to keep them for consistency.
|
|
|
|
// Set the flags on the resource so that the front-end can fetch
|
|
// and display request headers and cookies details asap.
|
|
lazy.NetworkUtils.setEventAsAvailable(resource, [
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS,
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES,
|
|
]);
|
|
|
|
this.networkEvents.set(resource.resourceId, networkEvent);
|
|
|
|
this.onAvailable([resource]);
|
|
|
|
if (memoryCacheKey) {
|
|
networkEventActor.addMemoryCacheData(channel, memoryCacheKey);
|
|
}
|
|
networkEventActor.addCacheDetails({ fromCache });
|
|
if (type == RESOURCE_TYPES.BLOCKED) {
|
|
lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
|
|
]);
|
|
this._emitUpdate(networkEvent);
|
|
} else if (type == RESOURCE_TYPES.CACHED) {
|
|
networkEventActor.addResponseStart({ channel, fromCache: true });
|
|
networkEventActor.addEventTimings(
|
|
0 /* totalTime */,
|
|
{} /* timings */,
|
|
{} /* offsets */
|
|
);
|
|
networkEventActor.addServerTimings({});
|
|
networkEventActor.addResponseContent({
|
|
mimeType: channel.contentType,
|
|
size: channel.contentLength,
|
|
text: "",
|
|
transferredSize: 0,
|
|
});
|
|
networkEventActor.addResponseContentComplete({});
|
|
} else if (type == RESOURCE_TYPES.DATA_CHANNEL) {
|
|
lazy.NetworkUtils.handleDataChannel(channel, networkEventActor);
|
|
}
|
|
}
|
|
|
|
onNetworkEventUpdate(updateResource) {
|
|
const networkEvent = this.networkEvents.get(updateResource.resourceId);
|
|
|
|
if (!networkEvent) {
|
|
return;
|
|
}
|
|
|
|
const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils;
|
|
const { resourceUpdates, receivedUpdates } = networkEvent;
|
|
|
|
switch (updateResource.updateType) {
|
|
case NETWORK_EVENT_TYPES.CACHE_DETAILS:
|
|
resourceUpdates.fromCache = updateResource.fromCache;
|
|
resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.RESPONSE_START: {
|
|
// For cached image requests channel.responseStatus is set to 200 as
|
|
// expected. However responseStatusText is empty. In this case fallback
|
|
// to the expected statusText "OK".
|
|
let statusText = updateResource.statusText;
|
|
if (!statusText && updateResource.status === "200") {
|
|
statusText = "OK";
|
|
}
|
|
resourceUpdates.httpVersion = updateResource.httpVersion;
|
|
resourceUpdates.status = updateResource.status;
|
|
resourceUpdates.statusText = statusText;
|
|
resourceUpdates.remoteAddress = updateResource.remoteAddress;
|
|
resourceUpdates.remotePort = updateResource.remotePort;
|
|
resourceUpdates.waitingTime = updateResource.waitingTime;
|
|
if (Number.isInteger(updateResource.priority)) {
|
|
resourceUpdates.priority = updateResource.priority;
|
|
}
|
|
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
|
|
NETWORK_EVENT_TYPES.RESPONSE_COOKIES,
|
|
NETWORK_EVENT_TYPES.RESPONSE_HEADERS,
|
|
]);
|
|
break;
|
|
}
|
|
case NETWORK_EVENT_TYPES.RESPONSE_CONTENT:
|
|
if (updateResource.contentSize !== undefined) {
|
|
resourceUpdates.contentSize = updateResource.contentSize;
|
|
}
|
|
resourceUpdates.mimeType = updateResource.mimeType;
|
|
resourceUpdates.transferredSize = updateResource.transferredSize;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.EVENT_TIMINGS:
|
|
resourceUpdates.totalTime = updateResource.totalTime;
|
|
break;
|
|
}
|
|
|
|
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
|
|
updateResource.updateType,
|
|
]);
|
|
|
|
receivedUpdates.push(updateResource.updateType);
|
|
|
|
// Here we explicitly call all three `add` helpers on each network event
|
|
// actor so in theory we could check only the last one to be called, ie
|
|
// responseContent.
|
|
const isResponseComplete =
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_START) &&
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) &&
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS);
|
|
|
|
if (isResponseComplete) {
|
|
// Lets add an event to clearly define the last update expected to be
|
|
// emitted. There will be no more updates after this.
|
|
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
|
|
NETWORK_EVENT_TYPES.RESPONSE_END,
|
|
]);
|
|
}
|
|
|
|
if (
|
|
updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START ||
|
|
updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT ||
|
|
isResponseComplete
|
|
) {
|
|
this._emitUpdate(networkEvent);
|
|
// clean up already sent updates
|
|
networkEvent.resourceUpdates = {};
|
|
}
|
|
}
|
|
|
|
_emitUpdate(networkEvent) {
|
|
this.onUpdated([
|
|
{
|
|
resourceId: networkEvent.resourceId,
|
|
resourceUpdates: networkEvent.resourceUpdates,
|
|
browsingContextID: networkEvent.browsingContextID,
|
|
innerWindowId: networkEvent.innerWindowId,
|
|
},
|
|
]);
|
|
}
|
|
|
|
onNetworkEventDestroyed(channelId) {
|
|
if (this.networkEvents.has(channelId)) {
|
|
this.networkEvents.delete(channelId);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.clear();
|
|
Services.obs.removeObserver(
|
|
this.httpFailedOpeningRequest,
|
|
"http-on-failed-opening-request"
|
|
);
|
|
|
|
Services.obs.removeObserver(
|
|
this.httpOnResourceCacheResponse,
|
|
"http-on-resource-cache-response"
|
|
);
|
|
|
|
Services.obs.removeObserver(
|
|
this.onDataChannelOpened,
|
|
"data-channel-opened"
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = NetworkEventContentWatcher;
|