Description
Self-service
- I'd be willing to implement a fix
Describe the bug
Yarn calls process.report.getReport
internally to retrieve a list of loaded shared libraries to determine the C library in use. This can be seen following the code path at
berry/packages/yarnpkg-core/sources/Project.ts
Lines 736 to 738 in 2615433
Yarn will grab this report and to generate the report, Node will go through all open handles in UV and retrieve information about them. Most importantly here, it will query information about all open socket handles. There handles are processed with https://github.com/nodejs/node/blob/9eb363a3e00dbba572756c7ed314273f17ea8e2e/src/node_report_utils.cc#L12.
- This function will try to resolve the name for at least the local IP address used on the socket (as I observed).
- This process is applied for all socket handles in series.
- This can potentially take minutes to fully generate.
As the above quoted comment implies, there is a larger issue on WSL. From my observations, WSL will, by default, register a very poor DNS resolver into the Linux distribution. This resolver is not only extremely slow, it also responds to all queries with an answer with a TTL of 0
. I expect that this causes the RDNS request for every single socket to go through the entire stack every single time.
While this behavior is generally prevented in yarn by calling out to the report generator early, when using plugins like upgrade-interactive
, a lot of sockets have been opened from the resolution process and this causes a very long hang when the report is being generated. This approach seems unreliable. Determination of platform specifics should not be hindered by DNS performance.
To reproduce
const https = require("node:https");
const SOCKET_COUNT = 10;
const sockets = [];
let allResolved = false;
const keepAliveAgent = new https.Agent({ keepAlive: true });
const performRequest = () => {
const socketPromise = new Promise((resolve) => {
https
.get(
{
agent: keepAliveAgent,
hostname: "registry.npmjs.org",
},
() => {
process.stdout.write(".");
resolve();
}
)
.on("error", (error) => {
console.error(error);
resolve();
});
});
sockets.push(socketPromise);
};
process.stdout.write(`${new Date().toISOString()} opening ${SOCKET_COUNT} sockets`);
for (let loops = 0; loops < SOCKET_COUNT; ++loops) {
performRequest();
}
Promise.all(sockets).then(() => {
allResolved = true;
console.log(
`\n${new Date().toISOString()} all sockets created. requesting report...`
);
process.report.getReport();
console.log(`${new Date().toISOString()} destroying client`);
keepAliveAgent.destroy();
console.log(`${new Date().toISOString()} end`);
});
const check = () => {
if (!allResolved) {
setTimeout(check, 200);
return;
}
};
check();
Environment
System:
OS: Linux 5.10 Ubuntu 20.04.3 LTS (Focal Fossa)
CPU: (12) x64 Intel(R) Core(TM) i7-6850K CPU @ 3.60GHz
Binaries:
Node: 18.12.0 - /tmp/xfs-ec34c936/node
Yarn: 1.22.19 - /tmp/xfs-ec34c936/yarn
npm: 8.19.2 - /usr/local/bin/npm
Additional context
This problem is amplified by poor WSL default behavior, but the underlying issue should apply generally.