Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: netwerk/test/unit/xpcshell.toml
/* 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
// timings coming from a connection it did not use. This happens when a
// transaction starts its own connection attempt but is dispatched to another
// connection before that attempt completes.
"use strict";
const { NodeHTTPSServer } = ChromeUtils.importESModule(
);
const { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
do_get_profile();
Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
let server;
function sleep(ms) {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
return new Promise(resolve => setTimeout(resolve, ms));
}
add_setup(async function () {
server = new NodeHTTPSServer();
await server.start(0, ["localhost"]);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
await server.stop();
});
// Busy loop, blocking the node event loop. While this request is being
// served, the TLS handshake of any other connection to this server can not
// make progress.
await server.registerPathHandler("/block", (req, resp) => {
let end = Date.now() + 500;
while (Date.now() < end) {
// Intentionally blocking.
}
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("blocked");
});
await server.registerPathHandler("/plain", (req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ok");
});
});
async function openChannel(path) {
let chan = NetUtil.newChannel({
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
}).QueryInterface(Ci.nsIHttpChannel);
await new Promise(resolve => {
chan.asyncOpen(
new ChannelListener((_req, _buf) => resolve(), null, CL_ALLOW_UNKNOWN_CL)
);
});
return chan.QueryInterface(Ci.nsITimedChannel);
}
function logTimings(label, tc) {
info(
`${label}: domainLookupStart=${tc.domainLookupStartTime} ` +
`domainLookupEnd=${tc.domainLookupEndTime} ` +
`connectStart=${tc.connectStartTime} ` +
`tcpConnectEnd=${tc.tcpConnectEndTime} ` +
`secureConnectionStart=${tc.secureConnectionStartTime} ` +
`connectEnd=${tc.connectEndTime} ` +
`requestStart=${tc.requestStartTime} ` +
`responseStart=${tc.responseStartTime}`
);
}
// The connection timings of a single request are either all absent, because the
// request reused a connection, or all present and in order, because it
// established one. A mix means the timings were taken from two different
// connections. Returns whether a connect phase was reported.
function assertCoherentTimings(label, tc) {
let connected = tc.connectStartTime > 0;
for (let name of [
"domainLookupStart",
"domainLookupEnd",
"secureConnectionStart",
"tcpConnectEnd",
"connectEnd",
]) {
Assert.equal(
tc[`${name}Time`] > 0,
connected,
`${label}: ${name} must be set iff connectStart is set`
);
}
if (connected) {
Assert.lessOrEqual(
tc.domainLookupEndTime,
tc.connectStartTime,
`${label}: domainLookupEnd <= connectStart`
);
Assert.lessOrEqual(
tc.connectStartTime,
tc.secureConnectionStartTime,
`${label}: connectStart <= secureConnectionStart`
);
Assert.lessOrEqual(
tc.secureConnectionStartTime,
tc.connectEndTime,
`${label}: secureConnectionStart <= connectEnd`
);
}
return connected;
}
async function resetConnections() {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
let tokensCache = Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(
Ci.nsISSLTokensCache
);
await tokensCache.asyncClearSSLExternalAndInternalSessionCache();
await sleep(1000);
}
async function doTestMovedTransactionTimings(name) {
let reusedCount = 0;
for (let round = 0; round < 3; round++) {
// Every round starts without a connection to the server, so the request
// below has to establish one and must report a connect phase.
await resetConnections();
// The first request keeps its connection (and the server) busy.
let blocked = openChannel("/block");
await sleep(100);
// This request opens a second connection, whose TLS handshake can not
// complete while the server is blocked. When the first connection becomes
// idle, this transaction is dispatched to it instead, leaving the second
// connection unused.
let moved = openChannel("/plain");
let blockedTimings = await blocked;
logTimings(`${name} round ${round} /block`, blockedTimings);
Assert.ok(
assertCoherentTimings(`${name} round ${round} /block`, blockedTimings),
`${name} round ${round} /block: a new connection reports a connect phase`
);
let movedTimings = await moved;
logTimings(`${name} round ${round} /plain (moved)`, movedTimings);
if (
!assertCoherentTimings(
`${name} round ${round} /plain (moved)`,
movedTimings
)
) {
reusedCount++;
}
// Later requests may pick up the unused connection from the idle pool.
for (let i = 0; i < 2; i++) {
let tc = await openChannel("/plain");
logTimings(`${name} round ${round} /plain (reuse ${i})`, tc);
assertCoherentTimings(`${name} round ${round} /plain (reuse ${i})`, tc);
}
}
// Not asserted: whether the second request loses the race for its own
// connection depends on the timing of the block above.
info(`${name}: ${reusedCount} of 3 moved requests reused a connection`);
}
add_task(async function test_moved_transaction_timings_happy_eyeballs() {
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
await doTestMovedTransactionTimings("happy eyeballs");
});
add_task(async function test_moved_transaction_timings_no_happy_eyeballs() {
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", false);
await doTestMovedTransactionTimings("no happy eyeballs");
});