From e92e7c206ba8e36347c0a2b6b096bacc8348ab5b Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Sat, 1 Mar 2025 22:10:28 -0800 Subject: [PATCH] swift run: Speed up fd closes on macOS/Linux Today, for everything that isn't the BSDs, we grab the maximum number of open fds the process supports and then loop through from 3 -> max, calling close(2) on everything. Even for a low open fd count of 65k this will typically result in 99+% of these close's being EBADF. At a 65k nofile RLIMIT the sluggishness is not really felt, but on systems that may have this in the millions it is extremely stark. `swift run` on a hello world program can take minutes before the program is actually ran. There's a couple ways to work around this, but there's also another issue in that actually closing the fds poses a problem in some cases with debug builds of libdispatch. There can be a race between libdispatch going to use the kqueue fd(s) and us closing them before the execve. Because of this, the most sane thing to do is instead of closing we can set all of the open fds as CLOEXEC. To do this efficiently on linux and macOS we can read /dev/fd and /proc/self/fd respectively and only close what's actually open. Below is the delta between two runs of `swift run` on a simple hello world program. The shell I'm running these in has a nofile rlimit of 1 billion. At 100 million it falls to about 20 seconds on my machine, and gets progressively smaller until the two approaches aren't really any different at all. With the patch: ``` Build of product 'fdwoo' complete! (0.23s) Hello, world! real 0m0.925s user 0m0.698s sys 0m0.129s ``` Without: ``` Build of product 'closerange' complete! (0.15s) Hello, world! real 2m43.203s user 0m47.357s sys 1m55.344s ``` Signed-off-by: Danny Canter --- Sources/Commands/SwiftRunCommand.swift | 47 +++++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/Sources/Commands/SwiftRunCommand.swift b/Sources/Commands/SwiftRunCommand.swift index 0504e6c63d2..785bacea1cf 100644 --- a/Sources/Commands/SwiftRunCommand.swift +++ b/Sources/Commands/SwiftRunCommand.swift @@ -331,7 +331,7 @@ public struct SwiftRunCommand: AsyncSwiftCommand { #if !os(Windows) // Dispatch will disable almost all asynchronous signals on its worker threads, and this is called from `async` // context. To correctly `exec` a freshly built binary, we will need to: - // 1. reset the signal masks + // 1. Reset the signal masks for i in 1..= from { + _ = fcntl(fd, F_SETFD, FD_CLOEXEC) + } + } + return + } + } + + let numFDs = sysconf(Int32(_SC_OPEN_MAX)) + for fd in from..