Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
15ff7a0
Fixes coreCount on Linux when using cgroup v2 with CFS throttling dis…
mitchellallison Dec 9, 2025
556574e
Make cpuSetPath, cpuSetPathV2 & cgroupV2MountPoint static lets.
mitchellallison Dec 15, 2025
264c9cf
Use lazily computed closures, as let variables are incompatible with …
mitchellallison Dec 15, 2025
91f100f
Remove redundant lazy.
mitchellallison Dec 15, 2025
a0e3fd9
Remove circular dependency between NIOCore & NIOFileSystem.
mitchellallison Dec 15, 2025
68af05c
Merge branch 'main' into fix-cgroups-v2-core-count
weissi Dec 15, 2025
c7e224a
Apply formatting fixes.
mitchellallison Dec 16, 2025
cb633ae
Fix Android/Static Linux compliation issues.
mitchellallison Dec 17, 2025
3109e63
Use correctly prefixed cgroup v2 mount point path, fix parsing of dou…
mitchellallison Dec 17, 2025
be58179
Switch cgroupVersion to be a stored property to align with other conv…
mitchellallison Dec 17, 2025
ef4d96c
Add back removed 'extern', fix up types for static/android SDKs, and …
mitchellallison Dec 17, 2025
1a2f263
Fix up second declaration.
mitchellallison Dec 17, 2025
35bb6a4
Fix up more build failures.
mitchellallison Dec 17, 2025
4d3bc93
Fix remaining build failures across Android/Static SDK, and fix up fo…
mitchellallison Dec 17, 2025
6ddfbb9
Merge branch 'main' into fix-cgroups-v2-core-count
mitchellallison Dec 17, 2025
20e9453
Workaround https://github.com/swiftlang/swift/issues/86149.
mitchellallison Dec 19, 2025
19ce396
Merge branch 'main' into fix-cgroups-v2-core-count
mitchellallison Dec 22, 2025
f415ee5
Merge branch 'main' into fix-cgroups-v2-core-count
Lukasa Jan 2, 2026
aeba647
Add back missing 'extern' for CNIOLinux_TMPFS_MAGIC declaration.
mitchellallison Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Sources/CNIOLinux/include/CNIOLinux.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
#include <sys/xattr.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <sched.h>
#include <stdbool.h>
#include <errno.h>
#include <pthread.h>
#include <netinet/ip.h>
#if __has_include(<linux/magic.h>)
#include <linux/magic.h>
#endif
#if __has_include(<linux/udp.h>)
#include <linux/udp.h>
#else
Expand Down Expand Up @@ -149,6 +153,27 @@ extern const unsigned long CNIOLinux_UTIME_NOW;

extern const long CNIOLinux_UDP_MAX_SEGMENTS;

// Filesystem magic constants for cgroup detection
#ifdef __ANDROID__
#if defined(__LP64__)
typedef uint64_t f_type_t;
#else
typedef uint32_t f_type_t;
#endif
#else
#ifdef __FSWORD_T_TYPE
typedef __fsword_t f_type_t;
#else
typedef unsigned long f_type_t;
#endif
#endif

extern const f_type_t CNIOLinux_TMPFS_MAGIC;
extern const f_type_t CNIOLinux_CGROUP2_SUPER_MAGIC;

// Workaround for https://github.com/swiftlang/swift/issues/86149
f_type_t CNIOLinux_statfs_ftype(const char *path);

// A workaround for incorrect nullability annotations in the Android SDK.
FTS *CNIOLinux_fts_open(char * const *path_argv, int options, int (*compar)(const FTSENT **, const FTSENT **));

Expand Down
18 changes: 18 additions & 0 deletions Sources/CNIOLinux/shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ const int CNIOLinux_AT_EMPTY_PATH = AT_EMPTY_PATH;
const unsigned long CNIOLinux_UTIME_OMIT = UTIME_OMIT;
const unsigned long CNIOLinux_UTIME_NOW = UTIME_NOW;

#ifndef TMPFS_MAGIC
#define TMPFS_MAGIC 0x01021994
#endif
#ifndef CGROUP2_SUPER_MAGIC
#define CGROUP2_SUPER_MAGIC 0x63677270
#endif

const f_type_t CNIOLinux_TMPFS_MAGIC = TMPFS_MAGIC;
const f_type_t CNIOLinux_CGROUP2_SUPER_MAGIC = CGROUP2_SUPER_MAGIC;

f_type_t CNIOLinux_statfs_ftype(const char *path) {
struct statfs fs;
f_type_t f_type = 0;
if (statfs(path, &fs) == 0) {
f_type = fs.f_type;
}
return f_type;
}

#ifdef UDP_MAX_SEGMENTS
const long CNIOLinux_UDP_MAX_SEGMENTS = UDP_MAX_SEGMENTS;
Expand Down
103 changes: 92 additions & 11 deletions Sources/NIOCore/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,110 @@

#if os(Linux) || os(Android)
import CNIOLinux

#if canImport(Android)
@preconcurrency import Android
#endif

enum Linux {
static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus"
static let cfsCpuMaxPath = "/sys/fs/cgroup/cpu.max"

private static func firstLineOfFile(path: String) throws -> Substring {
let fh = try NIOFileHandle(_deprecatedPath: path)
defer { try! fh.close() }
static let cpuSetPathV1 = "/sys/fs/cgroup/cpuset/cpuset.cpus"
static let cpuSetPathV2: String? = {
if let cgroupV2MountPoint = Self.cgroupV2MountPoint {
return "\(cgroupV2MountPoint)/cpuset.cpus"
}
return nil
}()

static let cgroupV2MountPoint: String? = {
guard
let fd = try? SystemCalls.open(file: "/proc/self/cgroup", oFlag: O_RDONLY, mode: NIOPOSIXFileMode(S_IRUSR))
else { return nil }
defer { try! SystemCalls.close(descriptor: fd) }
guard let lines = try? Self.readLines(descriptor: fd) else { return nil }

// Parse each line looking for cgroup v2 format: "0::/path"
for line in lines {
if let cgroupPath = Self.parseV2CgroupLine(line) {
return "/sys/fs/cgroup\(cgroupPath)"
}
}

return nil
}()

/// Returns the appropriate cpuset path based on the detected cgroup version
static let cpuSetPath: String? = {
guard let version = Self.cgroupVersion else { return nil }

switch version {
case .v1:
return cpuSetPathV1
case .v2:
return cpuSetPathV2
}
}()

/// Detects whether we're using cgroup v1 or v2
static let cgroupVersion: CgroupVersion? = {
guard let type = try? SystemCalls.statfs_ftype("/sys/fs/cgroup") else { return nil }

switch type {
case CNIOLinux_TMPFS_MAGIC:
return .v1
case CNIOLinux_CGROUP2_SUPER_MAGIC:
return .v2
default:
return nil
}
}()

enum CgroupVersion {
case v1
case v2
}

/// Parses a single line from /proc/self/cgroup to extract cgroup v2 path
internal static func parseV2CgroupLine(_ line: Substring) -> String? {
// Expected format is "0::/path"
let parts = line.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false)

guard parts.count == 3,
parts[0] == "0",
parts[1] == ""
else {
return nil
}

// Extract the path from parts[2]
return String(parts[2])
}

private static func readLines(descriptor: CInt) throws -> [Substring] {
// linux doesn't properly report /sys/fs/cgroup/* files lengths so we use a reasonable limit
var buf = ByteBufferAllocator().buffer(capacity: 1024)
try buf.writeWithUnsafeMutableBytes(minimumWritableBytes: buf.capacity) { ptr in
let res = try fh.withUnsafeFileDescriptor { fd -> CoreIOResult<ssize_t> in
try SystemCalls.read(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count)
}
let res = try SystemCalls.read(descriptor: descriptor, pointer: ptr.baseAddress!, size: ptr.count)

switch res {
case .processed(let n):
return n
case .wouldBlock:
preconditionFailure("read returned EWOULDBLOCK despite a blocking fd")
}
}
return String(buffer: buf).prefix(while: { $0 != "\n" })
return String(buffer: buf).split(separator: "\n")
}

private static func firstLineOfFile(path: String) throws -> Substring? {
guard let fd = try? SystemCalls.open(file: path, oFlag: O_RDONLY, mode: NIOPOSIXFileMode(S_IRUSR)) else {
return nil
}
defer { try! SystemCalls.close(descriptor: fd) }
return try? Self.readLines(descriptor: fd).first
}

private static func countCoreIds(cores: Substring) -> Int {
Expand All @@ -54,7 +135,7 @@ enum Linux {

static func coreCount(cpuset cpusetPath: String) -> Int? {
guard
let cpuset = try? firstLineOfFile(path: cpusetPath).split(separator: ","),
let cpuset = try? firstLineOfFile(path: cpusetPath).flatMap({ $0.split(separator: ",") }),
!cpuset.isEmpty
else { return nil }
return cpuset.map(countCoreIds).reduce(0, +)
Expand All @@ -67,11 +148,11 @@ enum Linux {
period periodPath: String = Linux.cfsPeriodPath
) -> Int? {
guard
let quota = try? Int(firstLineOfFile(path: quotaPath)),
let quota = try? firstLineOfFile(path: quotaPath).flatMap({ Int($0) }),
quota > 0
else { return nil }
guard
let period = try? Int(firstLineOfFile(path: periodPath)),
let period = try? firstLineOfFile(path: periodPath).flatMap({ Int($0) }),
period > 0
else { return nil }
return (quota - 1 + period) / period // always round up if fractional CPU quota requested
Expand Down
17 changes: 17 additions & 0 deletions Sources/NIOCore/SystemCallHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import CNIOWindows
#error("The system call helpers module was unable to identify your C library.")
#endif

#if os(Linux) || os(Android)
import CNIOLinux
#endif

#if os(Windows)
private let sysDup: @convention(c) (CInt) -> CInt = _dup
private let sysClose: @convention(c) (CInt) -> CInt = _close
Expand Down Expand Up @@ -232,5 +236,18 @@ enum SystemCalls {
}
}
#endif

#if os(Linux) || os(Android)
@inline(never)
@usableFromInline
internal static func statfs_ftype(
_ path: UnsafePointer<CChar>
) throws -> f_type_t {
try syscall(blocking: false) {
CNIOLinux_statfs_ftype(path)
}.result
}
#endif

#endif // !os(WASI)
}
25 changes: 20 additions & 5 deletions Sources/NIOCore/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,26 @@ public enum System: Sendable {
.map { $0.ProcessorMask.nonzeroBitCount }
.reduce(0, +)
#elseif os(Linux) || os(Android)
if let quota2 = Linux.coreCountCgroup2Restriction() {
return quota2
} else if let quota = Linux.coreCountCgroup1Restriction() {
return quota
} else if let cpusetCount = Linux.coreCount(cpuset: Linux.cpuSetPath) {
var cpuSetPath: String?

switch Linux.cgroupVersion {
case .v1:
if let quota = Linux.coreCountCgroup1Restriction() {
return quota
}
cpuSetPath = Linux.cpuSetPathV1
case .v2:
if let quota = Linux.coreCountCgroup2Restriction() {
return quota
}
cpuSetPath = Linux.cpuSetPathV2
case .none:
break
}

if let cpuSetPath,
let cpusetCount = Linux.coreCount(cpuset: cpuSetPath)
{
return cpusetCount
} else {
return sysconf(CInt(_SC_NPROCESSORS_ONLN))
Expand Down
Loading