Skip to content

Commit 2a9d6ad

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: 1. For Linux, on kernels 5.9 and up we have a handy friend named close_range, which is similar to closefrom on the BSDs which we already use. 2. If close_range doesn't exist we can read /proc/self/fd and close everything manually above 3. It seems from some trial runs there's really not all too many fds we hold open, so this is around the same runtime as close_range. A similar avenue exists on macOS in /dev/fd that we can use as well. This change employs both of them for Linux, and only /dev/fd for macOS. For Linux, we will try close_range first and if we get -1 (either for ENOSYS as the syscall doesn't exist, or for any other error) we'll fallback to the /proc method. 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 'closerange' complete! (0.17s) Hello, world! real 0m0.865s user 0m0.702s sys 0m0.110s ``` 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 2a9d6ad

File tree

5 files changed

+139
-18
lines changed

5 files changed

+139
-18
lines changed

Package.swift

+8
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ let package = Package(
515515
.product(name: "OrderedCollections", package: "swift-collections"),
516516
"Basics",
517517
"Build",
518+
"SPMExecHelpers",
518519
"CoreCommands",
519520
"PackageGraph",
520521
"PackageModelSyntax",
@@ -690,6 +691,13 @@ let package = Package(
690691
]
691692
),
692693

694+
// MARK: C helpers
695+
696+
.target(
697+
name: "SPMExecHelpers",
698+
dependencies: []
699+
),
700+
693701
// MARK: Additional Test Dependencies
694702

695703
.target(

Sources/Commands/SwiftRunCommand.swift

+22-18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import enum TSCUtility.Diagnostics
2626
import Android
2727
#endif
2828

29+
#if !os(Windows)
30+
import SPMExecHelpers
31+
#endif
32+
2933
/// An enumeration of the errors that can be generated by the run tool.
3034
private enum RunError: Swift.Error {
3135
/// The package manifest has no executable product.
@@ -331,36 +335,36 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
331335
#if !os(Windows)
332336
// Dispatch will disable almost all asynchronous signals on its worker threads, and this is called from `async`
333337
// context. To correctly `exec` a freshly built binary, we will need to:
334-
// 1. reset the signal masks
338+
// 1. Reset the signal masks
335339
for i in 1..<NSIG {
336340
signal(i, SIG_DFL)
337341
}
338342
var sig_set_all = sigset_t()
339343
sigfillset(&sig_set_all)
340344
sigprocmask(SIG_UNBLOCK, &sig_set_all, nil)
341345

342-
#if os(FreeBSD) || os(OpenBSD)
343-
#if os(FreeBSD)
344-
pthread_suspend_all_np()
345-
#endif
346-
closefrom(3)
347-
#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-
}
358-
#endif /* os(FreeBSD) || os(OpenBSD) */
346+
// 2. Close all file descriptors above stdio.
347+
closeFDsAbove(3)
359348
#endif
360349

350+
// All of the swift run modes expect to replace the process itself, with no fork in between.
361351
try TSCBasic.exec(path: path, args: args)
362352
}
363353

354+
#if !os(Windows)
355+
private func closeFDsAbove(_ from: UInt32) {
356+
// On platforms that have pthread_suspend_all_np, lets suspend all threads before
357+
// closing fds above stderr. This is mostly to handle a race that can occur with debug
358+
// builds of libdispatch, where we could be closing kqueue fds right before the exec.
359+
// pthread_suspend_all_np is not portable however, and is really only available readily
360+
// to call on FreeBSD.
361+
#if os(FreeBSD)
362+
pthread_suspend_all_np()
363+
#endif
364+
spm_close_fds_from(from);
365+
}
366+
#endif
367+
364368
public init() {}
365369
}
366370

Sources/SPMExecHelpers/exec_helpers.c

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#include <dirent.h>
14+
#include <unistd.h>
15+
#include <stdlib.h>
16+
#include <sys/resource.h>
17+
#include <limits.h>
18+
19+
#include "include/exec_helpers.h"
20+
21+
// glibc < 2.34 needs this. When running in a container it's possible the userland
22+
// doesn't have this defined in glibc, but the kernel supports the syscall. It's
23+
// very trivial to check via invoking it and checking for ENOSYS.
24+
#ifndef SYS_close_range
25+
#define SYS_close_range 436
26+
#endif
27+
28+
static int highest_open_fd_dir(const char *fd_dir) {
29+
DIR *dir = opendir(fd_dir);
30+
if (dir == NULL) {
31+
return -1;
32+
}
33+
34+
int highest_fd = 0;
35+
struct dirent *dir_entry = NULL;
36+
37+
while ((dir_entry = readdir(dir)) != NULL) {
38+
if (dir_entry->d_name[0] < '0' || dir_entry->d_name[0] > '9') {
39+
continue;
40+
}
41+
42+
int fd = atoi(dir_entry->d_name);
43+
if (fd > highest_fd) {
44+
highest_fd = fd;
45+
}
46+
}
47+
48+
closedir(dir);
49+
return highest_fd;
50+
}
51+
52+
static int get_highest_open_fd() {
53+
int highest_fd = -1;
54+
#if defined(__APPLE__)
55+
highest_fd = highest_open_fd_dir("/dev/fd");
56+
#elif defined(__linux__)
57+
highest_fd = highest_open_fd_dir("/proc/self/fd");
58+
#endif
59+
60+
if (highest_fd != -1) {
61+
return highest_fd;
62+
}
63+
64+
struct rlimit rl;
65+
if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
66+
return rl.rlim_cur;
67+
}
68+
69+
// Fallback to sysconf if our pal above didn't work.
70+
return sysconf(_SC_OPEN_MAX);
71+
}
72+
73+
void spm_close_fds_from(unsigned int from) {
74+
#if defined(__FreeBSD__) || defined(__OpenBSD__)
75+
closefrom(from);
76+
return;
77+
#endif
78+
79+
#if defined(__linux__)
80+
int ret = syscall(SYS_close_range, from, UINT_MAX, 0);
81+
if (ret == 0) {
82+
return;
83+
}
84+
#endif
85+
86+
int highest_fd = get_highest_open_fd();
87+
for (int i=from; i<highest_fd; i++) {
88+
close(i);
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#pragma once
14+
15+
void spm_close_fds_from(unsigned int from);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module SPMExecHelpers {
2+
header "exec_helpers.h"
3+
export *
4+
}

0 commit comments

Comments
 (0)