Skip to content

Commit 8ae49bd

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 8ae49bd

File tree

5 files changed

+130
-18
lines changed

5 files changed

+130
-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+
"CExecHelpers",
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: "CExecHelpers",
698+
dependencies: []
699+
),
700+
693701
// MARK: Additional Test Dependencies
694702

695703
.target(

Sources/CExecHelpers/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 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 close_fds_from(unsigned int from);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module CExecHelpers {
2+
header "exec_helpers.h"
3+
export *
4+
}

Sources/Commands/SwiftRunCommand.swift

+13-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import ArgumentParser
1414
import Basics
15+
import CExecHelpers
1516
import CoreCommands
1617
import Foundation
1718
import PackageGraph
@@ -331,36 +332,30 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
331332
#if !os(Windows)
332333
// Dispatch will disable almost all asynchronous signals on its worker threads, and this is called from `async`
333334
// context. To correctly `exec` a freshly built binary, we will need to:
334-
// 1. reset the signal masks
335+
// 1. Reset the signal masks
335336
for i in 1..<NSIG {
336337
signal(i, SIG_DFL)
337338
}
338339
var sig_set_all = sigset_t()
339340
sigfillset(&sig_set_all)
340341
sigprocmask(SIG_UNBLOCK, &sig_set_all, nil)
341342

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) */
343+
// 2. Close all file descriptors above stdio.
344+
closeFdsAbove(3)
359345
#endif
360346

361347
try TSCBasic.exec(path: path, args: args)
362348
}
363349

350+
#if !os(Windows)
351+
private func closeFdsAbove(_ from: UInt32) {
352+
#if os(FreeBSD)
353+
pthread_suspend_all_np()
354+
#endif
355+
close_fds_from(from);
356+
}
357+
#endif
358+
364359
public init() {}
365360
}
366361

0 commit comments

Comments
 (0)