Skip to content

Commit 8ee2d6d

Browse files
committed
Speed up isLocalPortActive with caching & server reuse
This only covers localhost, but that's quite a common case for testing scenarios. In practice this speeds up some proxying benchmarks about 30%.
1 parent 510023b commit 8ee2d6d

File tree

2 files changed

+58
-16
lines changed

2 files changed

+58
-16
lines changed

karma.conf.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ module.exports = function(config) {
3232
"dns2": require.resolve('./test/empty-stub.js'),
3333
"ws": require.resolve('./test/empty-stub.js'),
3434
"tmp-promise": require.resolve('./test/empty-stub.js'),
35-
"undici": require.resolve('./test/empty-stub.js')
35+
"undici": require.resolve('./test/empty-stub.js'),
36+
// socket-util has module-level Node-only code (net.createServer),
37+
// but is imported by some tests for isLocalIPv6Available:
38+
[require.resolve('./src/util/socket-util')]: require.resolve('./test/empty-stub.js')
3639
},
3740
fallback: {
3841
// With Webpack 5, we need explicit mocks for all node modules. Because the

src/util/socket-util.ts

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,64 @@ import {
2323
import { getSocketMetadataTags } from './socket-metadata';
2424
import { normalizeIP } from './ip-utils';
2525

26-
// Test if a local port for a given interface (IPv4/6) is currently in use
26+
// Test if a local port for a given interface (IPv4/6) is currently in use, by attempting
27+
// to bind to it. We reuse a single server instance and cache results to avoid creating
28+
// a new server on every request. Concurrent checks for the same port share one probe.
29+
const probeServer = net.createServer();
30+
const portActiveCache = new Map<string, { result: boolean, expires: number }>();
31+
const portActiveInFlight = new Map<string, Promise<boolean>>();
32+
const PORT_ACTIVE_CACHE_TTL = 5000; // 5 seconds
33+
2734
export async function isLocalPortActive(interfaceIp: '::1' | '127.0.0.1', port: number) {
2835
if (interfaceIp === '::1' && !isLocalIPv6Available) return false;
2936

30-
return new Promise((resolve) => {
31-
const server = net.createServer();
32-
server.listen({
33-
host: interfaceIp,
34-
port,
35-
ipv6Only: interfaceIp === '::1'
36-
});
37-
server.once('listening', () => {
38-
resolve(false);
39-
server.close(() => {});
40-
});
41-
server.once('error', (e) => {
42-
resolve(true);
37+
const cacheKey = `${interfaceIp}:${port}`;
38+
39+
const cached = portActiveCache.get(cacheKey);
40+
if (cached && cached.expires > Date.now()) {
41+
return cached.result;
42+
}
43+
44+
// Deduplicate concurrent checks for the same address:port
45+
let probe = portActiveInFlight.get(cacheKey);
46+
if (!probe) {
47+
probe = probePort(interfaceIp, port);
48+
portActiveInFlight.set(cacheKey, probe);
49+
probe.then((result) => {
50+
portActiveCache.set(cacheKey, { result, expires: Date.now() + PORT_ACTIVE_CACHE_TTL });
51+
portActiveInFlight.delete(cacheKey);
4352
});
44-
});
53+
}
54+
55+
return probe;
56+
}
57+
58+
// Serialized via the in-flight map above — only one probe runs at a time per key,
59+
// and different keys won't collide because they share the single probeServer sequentially
60+
// via the listen/close cycle.
61+
let probeServerReady = Promise.resolve();
62+
63+
function probePort(interfaceIp: string, port: number): Promise<boolean> {
64+
const result = probeServerReady.then(() =>
65+
new Promise<boolean>((resolve) => {
66+
probeServer.listen({
67+
host: interfaceIp,
68+
port,
69+
ipv6Only: interfaceIp === '::1'
70+
});
71+
probeServer.once('listening', () => {
72+
probeServer.close(() => resolve(false));
73+
});
74+
probeServer.once('error', () => {
75+
resolve(true);
76+
});
77+
})
78+
);
79+
80+
// Chain so the next probe waits for this one to fully complete
81+
probeServerReady = result.then(() => {});
82+
83+
return result;
4584
}
4685

4786
// This file imported in browsers etc as it's used in handlers, but none of these methods are used

0 commit comments

Comments
 (0)