tor-browser

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

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:
Mdevtools/client/netmonitor/test/browser.toml | 2++
Adevtools/client/netmonitor/test/browser_net_throttling_disable_unblocks_requests.js | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdevtools/shared/network-observer/NetworkObserver.sys.mjs | 14+++++++++++++-
Mdevtools/shared/network-observer/NetworkThrottleManager.sys.mjs | 52++++++++++++++++++++++++++++++++++++++--------------
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; } } }