Skip to content

Commit e92e7c2

Browse files
committed
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 <[email protected]>
1 parent 70862aa commit e92e7c2

File tree

1 file changed

+34
-13
lines changed

1 file changed

+34
-13
lines changed

Sources/Commands/SwiftRunCommand.swift

+34-13
Original file line numberDiff line numberDiff line change
@@ -331,36 +331,58 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
331331
#if !os(Windows)
332332
// Dispatch will disable almost all asynchronous signals on its worker threads, and this is called from `async`
333333
// context. To correctly `exec` a freshly built binary, we will need to:
334-
// 1. reset the signal masks
334+
// 1. Reset the signal masks
335335
for i in 1..<NSIG {
336336
signal(i, SIG_DFL)
337337
}
338338
var sig_set_all = sigset_t()
339339
sigfillset(&sig_set_all)
340340
sigprocmask(SIG_UNBLOCK, &sig_set_all, nil)
341341

342+
// 2. Close or set CLOEXEC on all fds above stdio.
343+
//
344+
// On platforms that have pthread_suspend_all_np, lets suspend all threads before
345+
// closing fds above stderr. This is mostly to handle a race that can occur with debug
346+
// builds of libdispatch, where we could be closing kqueue fds right before the exec.
347+
// pthread_suspend_all_np is not portable however, and is really only available readily
348+
// to call on FreeBSD.
342349
#if os(FreeBSD) || os(OpenBSD)
343350
#if os(FreeBSD)
344351
pthread_suspend_all_np()
345352
#endif
346353
closefrom(3)
347354
#else
348-
#if os(Android)
349-
let number_fds = Int32(sysconf(_SC_OPEN_MAX))
350-
#else
351-
let number_fds = getdtablesize()
352-
#endif /* os(Android) */
353-
354-
// 2. close all file descriptors.
355-
for i in 3..<number_fds {
356-
close(i)
357-
}
355+
// On other platforms lets do CLOEXEC and try /dev/fd and /proc/self/fd, and
356+
// fall back to sysconf if those fail.
357+
cloexecFDsFrom(3)
358358
#endif /* os(FreeBSD) || os(OpenBSD) */
359-
#endif
359+
#endif /* !os(Windows) */
360360

361+
// All of the swift run modes expect to replace the process itself, with no fork in between.
361362
try TSCBasic.exec(path: path, args: args)
362363
}
363364

365+
#if !os(Windows)
366+
private func cloexecFDsFrom(_ from: Int32) {
367+
let fdDirs = ["/proc/self/fd", "/dev/fd"]
368+
for fdDir in fdDirs {
369+
if let fds = try? FileManager.default.contentsOfDirectory(atPath: fdDir) {
370+
for fdStr in fds {
371+
if let fd = Int32(fdStr), fd >= from {
372+
_ = fcntl(fd, F_SETFD, FD_CLOEXEC)
373+
}
374+
}
375+
return
376+
}
377+
}
378+
379+
let numFDs = sysconf(Int32(_SC_OPEN_MAX))
380+
for fd in from..<Int32(numFDs) {
381+
_ = fcntl(fd, F_SETFD, FD_CLOEXEC)
382+
}
383+
}
384+
#endif
385+
364386
public init() {}
365387
}
366388

@@ -369,4 +391,3 @@ private extension Basics.Diagnostic {
369391
.warning("'swift run file.swift' command to interpret swift files is deprecated; use 'swift file.swift' instead")
370392
}
371393
}
372-

0 commit comments

Comments
 (0)