[Bug 60465] New: nsiproxy never drops vanished network interfaces; GetAdaptersAddresses and InternetGetConnectedState slow down as containers come and go
http://bugs.winehq.org/show_bug.cgi?id=60465 Bug ID: 60465 Summary: nsiproxy never drops vanished network interfaces; GetAdaptersAddresses and InternetGetConnectedState slow down as containers come and go Product: Wine Version: 11.19 Hardware: x86-64 OS: Linux Status: UNCONFIRMED Severity: normal Priority: P2 Component: iphlpapi Assignee: wine-bugs@list.winehq.org Reporter: alexfunk1222@gmail.com Target Milestone: --- Distribution: --- The Witcher 3 (Steam 292030, 4.x "next-gen" build, DX12) calls wininet!InternetGetConnectedState about 10 times a second on its main thread. Under Wine each call rebuilds the full adapter list, and the cost keeps growing on a host where network interfaces come and go (Docker/Podman containers, VMs, VPNs), because nsiproxy never forgets an interface. In the game (Proton) this was a 35 ms stall every ~100 ms: 55-63 fps with a 1% low of 17-20, against 88 fps and a 1% low of 63 with the result cached for 2 s in a proxy DLL. Three parts of the current code add up (read at master 63f62f7cd696; Proton 10.0 and 11.0 are the same): 1. dlls/nsiproxy.sys/ndis.c, update_if_table(): adds each interface if_nameindex() returns and never removes one. An interface that is gone stays in if_list for the life of winedevice.exe, and GetAdaptersAddresses keeps returning it. 2. ifinfo_enumerate_all() calls ifinfo_fill_dynamic() for every entry in if_list, and each call opens and scans all of /proc/net/dev (plus a socket, SIOCGIFFLAGS, SIOCGIFMTU and a /sys carrier read). Every /proc/net/dev read makes the kernel collect statistics for every interface on the host, so one enumeration costs (entries in if_list) x (host interfaces) of that work. Some drivers are slow at it: here the Realtek r8127 driver dumps hardware counters, and one /proc/net/dev read takes 0.059 ms natively. 3. dlls/wininet/internet.c, InternetGetConnectedStateExW(): no cache, and GetAdaptersAddresses is called twice per call (a size probe with a NULL buffer, then the fill); GetAdaptersAddresses builds the whole list for the size probe too. That comes to about four ifinfo enumerations per call. adapters_addresses_alloc() also makes one ConvertInterfaceLuidToGuid() round trip to winedevice per interface, though stat[i].if_guid is already in the table it just fetched. Reproducer (attached, also at https://github.com/a-funk/GameSpark/blob/4e39f2711db06bfaeb0794e23e2f0ca823f...): times InternetGetConnectedState, GetAdaptersAddresses and ConvertInterfaceLuidToGuid; then polls InternetGetConnectedState at 10 Hz for the given number of seconds and times them again. x86_64-w64-mingw32-gcc -O2 -mwindows -o igcs_cost.exe igcs_cost.c -lwininet -liphlpapi wine igcs_cost.exe 50 out.txt 150 & for i in $(seq 70); do docker run --rm alpine true; done # while it polls Results, each in a fresh prefix on a host with 11 interfaces: adapters InternetGetConnectedState GetAdaptersAddresses Wine 11.19 (WineHQ Ubuntu 24.04 build) start 11 5.0 ms 2.5 ms after 70 containers came and went 115 39.8 ms 21.3 ms Proton Experimental 11.0-20261001 start 11 7.0 ms 3.5 ms after 70 containers came and went 100 45.4 ms 21.3 ms At the end, all but 11 of the adapters no longer existed. In the game, a +iphlpapi,+nsi trace showed 81 ConvertInterfaceLuidToGuid calls per GetAdaptersAddresses (1,047,767 over 12,879), i.e. 81 entries against 11 real interfaces, while another workload on the host started and removed 634 containers. Setup and caveats: NVIDIA DGX Spark (ARM64, Ubuntu 24.04, kernel 6.17.0-1029-nvidia). The x86-64 Wine builds run under the FEX emulator, so Wine's own code runs translated; the /proc/net/dev reads run natively and are roughly half to two thirds of the cost after the churn (estimated from the native read time). Not measured on an x86-64 host, though the code paths above are the same. Possible fixes, smallest first: - update_if_table(): drop entries whose index is no longer in the if_nameindex() result. - ifinfo_enumerate_all(): read /proc/net/dev once per enumeration, not once per interface. - adapters_addresses_alloc(): use stat[i].if_guid instead of ConvertInterfaceLuidToGuid(). - InternetGetConnectedStateExW(): try a buffer sized for typical hosts first so GetAdaptersAddresses usually runs once, or cache the answer briefly. The analysis and the test program were prepared with help from an AI assistant (Claude). The test program is only a reproducer and is not proposed for inclusion in Wine; the fixes above are described rather than written as code, in line with the project's policy on LLM-generated code. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60465 --- Comment #1 from Alex <alexfunk1222@gmail.com> --- Created attachment 82291 --> http://bugs.winehq.org/attachment.cgi?id=82291 Reproducer: times InternetGetConnectedState before and after polling at 10 Hz -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60465 --- Comment #2 from Hans Leidekker <hans@meelstraat.net> --- nsiproxy.sys only supports detecting address changes currently. It could be extended to watch interface changes, which will be needed to implement NotifyIpInterfaceChange() as well. Some form of caching probably also makes sense. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
participants (1)
-
WineHQ Bugzilla