perf: cache compute.host_ip + add UDPLink.create timing log

host_ip resolved socket.gethostbyname on every access with no cache.
get_ip_on_same_subnet touches host_ip 2-4 times per link, so opening a
2500-link project issued thousands of blocking DNS calls inline on the
event loop — freezing all concurrent link coroutines each time. This is
the most likely cause of the 12-link/s throughput (1000x below what
Pool(concurrency=100) should deliver) and the burst+pause pattern.

- compute.host_ip: cache the resolution in _host_ip_cache, invalidate
  on host setter change
- UDPLink.create: timing log splitting get_ip / ports / nio so the next
  project-open confirms where time actually goes
This commit is contained in:
YueGuobin 2026-08-10 23:20:40 +08:00
parent b155980402
commit 96d4d82716
No known key found for this signature in database
2 changed files with 24 additions and 4 deletions

View File

@ -98,6 +98,10 @@ class Compute:
self.name = name
# Cache of interfaces on remote host
self._interfaces_cache = None
# Cached resolution of self._host — socket.gethostbyname is a blocking
# call; resolving it on every host_ip access (several times per link
# via get_ip_on_same_subnet) freezes the event loop for all coroutines.
self._host_ip_cache = None
self._connection_failure = 0
def _session(self):
@ -224,14 +228,17 @@ class Compute:
"""
Return the IP associated to the host
"""
try:
return socket.gethostbyname(self._host)
except socket.gaierror:
return "0.0.0.0"
if self._host_ip_cache is None:
try:
self._host_ip_cache = socket.gethostbyname(self._host)
except socket.gaierror:
self._host_ip_cache = "0.0.0.0"
return self._host_ip_cache
@host.setter
def host(self, host):
self._host = host
self._host_ip_cache = None # invalidate; re-resolve on next access
if self._console_host is None:
self._console_host = host

View File

@ -17,6 +17,8 @@
import asyncio
import time
import logging
from .controller_error import ControllerError, ControllerNotFoundError
from .link import Link, _UNSET
@ -32,6 +34,9 @@ _MARKER_CAPABLE_TYPES = frozenset({
})
log = logging.getLogger(__name__)
class UDPLink(Link):
def __init__(self, project, link_id=None):
super().__init__(project, link_id=link_id)
@ -96,10 +101,12 @@ class UDPLink(Link):
port_number2 = self._nodes[1]["port_number"]
# Get an IP allowing communication between both host
_t0 = time.perf_counter()
try:
(node1_host, node2_host) = await node1.compute.get_ip_on_same_subnet(node2.compute)
except ValueError as e:
raise ControllerError(f"Cannot get an IP address on same subnet: {e}")
_t1 = time.perf_counter()
# Reserve a UDP port on both sides in parallel. Pre-allocated ports
# (used during batch project loading) are popped from memory; otherwise
@ -114,6 +121,7 @@ class UDPLink(Link):
self._node1_port, self._node2_port = await asyncio.gather(
_allocate_port(node1.compute), _allocate_port(node2.compute)
)
_t2 = time.perf_counter()
node1_filters, node2_filters = self._get_node_filters(node1, node2)
node1_markers, node2_markers = self._get_node_markers(node1, node2)
@ -171,6 +179,11 @@ class UDPLink(Link):
if cleanup:
await asyncio.gather(*cleanup, return_exceptions=True)
raise errors[0]
_t3 = time.perf_counter()
log.info(
"UDPLink.create timing get_ip=%.3fms ports=%.3fms nio=%.3fms total=%.3fms",
1000 * (_t1 - _t0), 1000 * (_t2 - _t1), 1000 * (_t3 - _t2), 1000 * (_t3 - _t0)
)
self._created = True
# New links automatically inherit every active project-level marker
# definition so the user doesn't have to reconfigure.