commit 56ef387ebe0de648894bc7ec90a0a32af263cd30
parent 1ebd20dd1cf0af009121e2bb47dd8eee65aebb30
Author: Julian Descottes <jdescottes@mozilla.com>
Date: Thu, 18 Dec 2025 22:42:52 +0000
Bug 2003238 - [devtools] Unthrottle pending requests when throttling is disabled r=devtools-reviewers,bomsy
Differential Revision: https://phabricator.services.mozilla.com/D276003
Diffstat:
4 files changed, 130 insertions(+), 15 deletions(-)
diff --git a/devtools/client/netmonitor/test/browser.toml b/devtools/client/netmonitor/test/browser.toml
@@ -508,6 +508,8 @@ fail-if = [
["browser_net_throttling_cached.js"]
+["browser_net_throttling_disable_unblocks_requests.js"]
+
["browser_net_throttling_menu.js"]
["browser_net_throttling_profiles.js"]
diff --git a/devtools/client/netmonitor/test/browser_net_throttling_disable_unblocks_requests.js b/devtools/client/netmonitor/test/browser_net_throttling_disable_unblocks_requests.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Network throttling test: check that disabling throttling allows previously
+// blocked requests to go back to unthrottled and complete quickly.
+
+"use strict";
+
+const httpServer = createTestHTTPServer();
+httpServer.registerPathHandler(`/`, function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`<meta charset=utf8><h1>Test throttling profiles</h1>`);
+});
+
+// The "data" path takes a size query parameter and will return a body of the
+// requested size.
+httpServer.registerPathHandler("/data", function (request, response) {
+ const size = request.queryString.match(/size=(\d+)/)[1];
+ response.setHeader("Content-Type", "text/plain");
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ const body = new Array(size * 1).join("a");
+ response.bodyOutputStream.write(body, body.length);
+});
+
+const TEST_URI = `http://localhost:${httpServer.identity.primaryPort}/`;
+
+add_task(async function () {
+ const { monitor } = await initNetMonitor(TEST_URI, { requestCount: 1 });
+ const { store, windowRequire, connector } = monitor.panelWin;
+ const { updateNetworkThrottling } = connector;
+ const { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index"
+ );
+
+ const throttleProfile = {
+ latency: 100,
+ download: 1,
+ upload: 10000,
+ };
+
+ info("Enable very slow throttling");
+ await updateNetworkThrottling(true, throttleProfile);
+
+ // Start waiting for 2 network events.
+ const onNetworkEvents = waitForNetworkEvents(monitor, 2);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.fetch("data?size=100");
+ });
+
+ info("Wait until the request is visible in the UI and then wait for 100ms");
+ const throttledRequest = await waitFor(
+ () => getSortedRequests(store.getState())[0]
+ );
+ await wait(100);
+ ok(!throttledRequest.eventTimings, "Request is still not finished");
+
+ info("Disable network throttling");
+ await updateNetworkThrottling(false);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.fetch("data?size=100");
+ });
+
+ await waitForRequestData(store, ["eventTimings"]);
+ // The throttled request should be unblocked after disabling throttling.
+ await onNetworkEvents;
+
+ const requestItem = getSortedRequests(store.getState())[1];
+ Assert.less(
+ requestItem.eventTimings.timings.receive,
+ 1000,
+ "download reported as taking less than one second"
+ );
+
+ await teardown(monitor);
+});
diff --git a/devtools/shared/network-observer/NetworkObserver.sys.mjs b/devtools/shared/network-observer/NetworkObserver.sys.mjs
@@ -314,9 +314,21 @@ export class NetworkObserver {
return this.#throttleData;
}
+ /**
+ * Update the network throttling configuration.
+ *
+ * @param {object|null} value
+ * The network throttling configuration object, or null if throttling
+ * should be disabled.
+ */
setThrottleData(value) {
this.#throttleData = value;
- // Clear out any existing throttlers
+
+ // If value is null, the user is disabling throttling, destroy the previous
+ // throttler.
+ if (this.#throttler && value === null) {
+ this.#throttler.destroy();
+ }
this.#throttler = null;
}
diff --git a/devtools/shared/network-observer/NetworkThrottleManager.sys.mjs b/devtools/shared/network-observer/NetworkThrottleManager.sys.mjs
@@ -35,6 +35,7 @@ class NetworkThrottleListener {
#pendingException;
#queue;
#responseStarted;
+ #shouldStopThrottling;
/**
* Construct a new nsIStreamListener that buffers data and provides a
@@ -54,6 +55,13 @@ class NetworkThrottleListener {
this.#pendingException = null;
this.#queue = queue;
this.#responseStarted = false;
+ this.#shouldStopThrottling = false;
+ }
+
+ stopThrottling() {
+ // When the shouldStopThrottling flag is flipped the next call to
+ // sendSomeData will bypass throttling and send all data immediately.
+ this.#shouldStopThrottling = true;
}
/**
@@ -130,7 +138,7 @@ class NetworkThrottleListener {
return { length: 0, done: true };
}
- if (bytesPermitted > count) {
+ if (bytesPermitted > count || this.#shouldStopThrottling) {
bytesPermitted = count;
}
@@ -276,7 +284,6 @@ class NetworkThrottleListener {
}
class NetworkThrottleQueue {
- #downloadQueue;
#latencyMax;
#latencyMean;
#maxBPS;
@@ -284,6 +291,7 @@ class NetworkThrottleQueue {
#pendingRequests;
#previousReads;
#pumping;
+ #throttleListeners;
/**
* Construct a new queue that can be used to throttle the network for
@@ -301,12 +309,18 @@ class NetworkThrottleQueue {
this.#latencyMax = latencyMax;
this.#pendingRequests = new Set();
- this.#downloadQueue = [];
+ this.#throttleListeners = [];
this.#previousReads = [];
this.#pumping = false;
}
+ destroy() {
+ for (const listener of this.#throttleListeners) {
+ listener.stopThrottling();
+ }
+ }
+
/**
* A helper function that lets the indicating listener start sending
* data. This is called after the initial round trip time for the
@@ -317,7 +331,7 @@ class NetworkThrottleQueue {
this.#pendingRequests.delete(throttleListener);
const count = throttleListener.pendingCount();
for (let i = 0; i < count; ++i) {
- this.#downloadQueue.push(throttleListener);
+ this.#throttleListeners.push(throttleListener);
}
this.#pump();
}
@@ -353,13 +367,13 @@ class NetworkThrottleQueue {
if (totalBytes < thisSliceBytes) {
thisSliceBytes -= totalBytes;
let readThisTime = 0;
- while (thisSliceBytes > 0 && this.#downloadQueue.length) {
+ while (thisSliceBytes > 0 && this.#throttleListeners.length) {
const { length, done } =
- this.#downloadQueue[0].sendSomeData(thisSliceBytes);
+ this.#throttleListeners[0].sendSomeData(thisSliceBytes);
thisSliceBytes -= length;
readThisTime += length;
if (done) {
- this.#downloadQueue.shift();
+ this.#throttleListeners.shift();
}
}
this.#previousReads.push({ when: now, numBytes: readThisTime });
@@ -367,7 +381,7 @@ class NetworkThrottleQueue {
// If there is more data to download, then schedule ourselves for
// one second after the oldest previous read.
- if (this.#downloadQueue.length) {
+ if (this.#throttleListeners.length) {
const when = this.#previousReads[0].when + 1000;
lazy.setTimeout(this.#pump.bind(this), when - now);
}
@@ -410,7 +424,7 @@ class NetworkThrottleQueue {
*/
dataAvailable(throttleListener) {
if (!this.#pendingRequests.has(throttleListener)) {
- this.#downloadQueue.push(throttleListener);
+ this.#throttleListeners.push(throttleListener);
this.#pump();
}
}
@@ -434,6 +448,7 @@ class NetworkThrottleQueue {
*/
export class NetworkThrottleManager {
#downloadQueue;
+ #uploadQueue;
constructor({
latencyMean,
@@ -454,12 +469,21 @@ export class NetworkThrottleManager {
);
}
if (uploadBPSMax <= 0 && uploadBPSMean <= 0) {
- this.uploadQueue = null;
+ this.#uploadQueue = null;
} else {
- this.uploadQueue = Cc[
+ this.#uploadQueue = Cc[
"@mozilla.org/network/throttlequeue;1"
].createInstance(Ci.nsIInputChannelThrottleQueue);
- this.uploadQueue.init(uploadBPSMean, uploadBPSMax);
+ this.#uploadQueue.init(uploadBPSMean, uploadBPSMax);
+ }
+ }
+
+ destroy() {
+ // The #uploadQueue is not a NetworkThrottleQueue and at the moment, there
+ // is no way to destroy it.
+ if (this.#downloadQueue !== null) {
+ this.#downloadQueue.destroy();
+ this.#downloadQueue = null;
}
}
@@ -487,9 +511,9 @@ export class NetworkThrottleManager {
* @param {nsITraceableChannel} channel the channel to manage
*/
manageUpload(channel) {
- if (this.uploadQueue) {
+ if (this.#uploadQueue) {
channel = channel.QueryInterface(Ci.nsIThrottledInputChannel);
- channel.throttleQueue = this.uploadQueue;
+ channel.throttleQueue = this.#uploadQueue;
}
}
}