Skip to content

Commit 3e7ee58

Browse files
authored
Merge pull request #2931 from spevans/pr_sr_12833
2 parents 5102c16 + 57bc80c commit 3e7ee58

File tree

3 files changed

+125
-13
lines changed

3 files changed

+125
-13
lines changed

Sources/Foundation/ProcessInfo.swift

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,17 @@ open class ProcessInfo: NSObject {
250250

251251
internal let _processorCount: Int = Int(__CFProcessorCount())
252252
open var processorCount: Int { _processorCount }
253-
253+
254+
#if os(Linux)
255+
// coreCount takes into account cgroup information eg if running under Docker
256+
// __CFActiveProcessorCount uses sched_getaffinity() and sysconf(_SC_NPROCESSORS_ONLN)
257+
internal let _activeProcessorCount: Int = ProcessInfo.coreCount() ?? Int(__CFActiveProcessorCount())
258+
#else
254259
internal let _activeProcessorCount: Int = Int(__CFActiveProcessorCount())
260+
#endif
261+
255262
open var activeProcessorCount: Int { _activeProcessorCount }
256-
263+
257264
internal let _physicalMemory = __CFMemorySize()
258265
open var physicalMemory: UInt64 {
259266
return _physicalMemory
@@ -293,6 +300,63 @@ open class ProcessInfo: NSObject {
293300
open var fullUserName: String {
294301
return NSFullUserName()
295302
}
303+
304+
305+
#if os(Linux)
306+
// Support for CFS quotas for cpu count as used by Docker.
307+
// Based on swift-nio code, https://github.com/apple/swift-nio/pull/1518
308+
private static let cfsQuotaPath = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
309+
private static let cfsPeriodPath = "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
310+
private static let cpuSetPath = "/sys/fs/cgroup/cpuset/cpuset.cpus"
311+
312+
private static func firstLineOfFile(path: String) throws -> Substring {
313+
let data = try Data(contentsOf: URL(fileURLWithPath: path))
314+
if let string = String(data: data, encoding: .utf8), let line = string.split(separator: "\n").first {
315+
return line
316+
} else {
317+
return ""
318+
}
319+
}
320+
321+
// These are internal access for testing
322+
static func countCoreIds(cores: Substring) -> Int {
323+
let ids = cores.split(separator: "-", maxSplits: 1)
324+
guard let first = ids.first.flatMap({ Int($0, radix: 10) }),
325+
let last = ids.last.flatMap({ Int($0, radix: 10) }),
326+
last >= first
327+
else { preconditionFailure("cpuset format is incorrect") }
328+
return 1 + last - first
329+
}
330+
331+
static func coreCount(cpuset cpusetPath: String) -> Int? {
332+
guard let cpuset = try? firstLineOfFile(path: cpusetPath).split(separator: ","),
333+
!cpuset.isEmpty
334+
else { return nil }
335+
336+
return cpuset.map(countCoreIds).reduce(0, +)
337+
}
338+
339+
static func coreCount(quota quotaPath: String, period periodPath: String) -> Int? {
340+
guard let quota = try? Int(firstLineOfFile(path: quotaPath)),
341+
quota > 0
342+
else { return nil }
343+
guard let period = try? Int(firstLineOfFile(path: periodPath)),
344+
period > 0
345+
else { return nil }
346+
347+
return (quota - 1 + period) / period // always round up if fractional CPU quota requested
348+
}
349+
350+
private static func coreCount() -> Int? {
351+
if let quota = coreCount(quota: cfsQuotaPath, period: cfsPeriodPath) {
352+
return quota
353+
} else if let cpusetCount = coreCount(cpuset: cpuSetPath) {
354+
return cpusetCount
355+
} else {
356+
return nil
357+
}
358+
}
359+
#endif
296360
}
297361

298362
// SPI for TestFoundation

Tests/Foundation/Tests/TestProcessInfo.swift

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
11+
#if canImport(SwiftFoundation) && !DEPLOYMENT_RUNTIME_OBJC
12+
@testable import SwiftFoundation
13+
#else
14+
@testable import Foundation
15+
#endif
16+
#endif
17+
1018
class TestProcessInfo : XCTestCase {
1119

12-
static var allTests: [(String, (TestProcessInfo) -> () throws -> Void)] {
13-
return [
14-
("test_operatingSystemVersion", test_operatingSystemVersion ),
15-
("test_processName", test_processName ),
16-
("test_globallyUniqueString", test_globallyUniqueString ),
17-
("test_environment", test_environment),
18-
]
19-
}
20-
2120
func test_operatingSystemVersion() {
2221
let processInfo = ProcessInfo.processInfo
2322
let versionString = processInfo.operatingSystemVersionString
@@ -134,4 +133,51 @@ class TestProcessInfo : XCTestCase {
134133
XCTAssertEqual(env["var4"], "x=")
135134
XCTAssertEqual(env["var5"], "=x=")
136135
}
136+
137+
138+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT && os(Linux)
139+
func test_cfquota_parsing() throws {
140+
141+
let tests = [
142+
("50000", "100000", 1),
143+
("100000", "100000", 1),
144+
("100000\n", "100000", 1),
145+
("100000", "100000\n", 1),
146+
("150000", "100000", 2),
147+
("200000", "100000", 2),
148+
("-1", "100000", nil),
149+
("100000", "-1", nil),
150+
("", "100000", nil),
151+
("100000", "", nil),
152+
("100000", "0", nil)
153+
]
154+
155+
try withTemporaryDirectory() { (_, tempDirPath) -> Void in
156+
try tests.forEach { quota, period, count in
157+
let (fd1, quotaPath) = try _NSCreateTemporaryFile(tempDirPath + "/quota")
158+
FileHandle(fileDescriptor: fd1, closeOnDealloc: true).write(quota)
159+
160+
let (fd2, periodPath) = try _NSCreateTemporaryFile(tempDirPath + "/period")
161+
FileHandle(fileDescriptor: fd2, closeOnDealloc: true).write(period)
162+
XCTAssertEqual(ProcessInfo.coreCount(quota: quotaPath, period: periodPath), count)
163+
}
164+
}
165+
}
166+
#endif
167+
168+
169+
static var allTests: [(String, (TestProcessInfo) -> () throws -> Void)] {
170+
var tests: [(String, (TestProcessInfo) -> () throws -> ())] = [
171+
("test_operatingSystemVersion", test_operatingSystemVersion ),
172+
("test_processName", test_processName ),
173+
("test_globallyUniqueString", test_globallyUniqueString ),
174+
("test_environment", test_environment),
175+
]
176+
177+
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT && os(Linux)
178+
tests.append(contentsOf: [ ("test_cfquota_parsing", test_cfquota_parsing) ])
179+
#endif
180+
181+
return tests
182+
}
137183
}

Tests/Foundation/Utilities.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,10 @@ public func withTemporaryDirectory<R>(functionName: String = #function, block: (
667667
throw TestError.unexpectedNil
668668
}
669669

670-
let fname = String(functionName[..<idx])
671-
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(testBundleName()).appendingPathComponent(fname).appendingPathComponent(NSUUID().uuidString)
670+
// Create the temporary directory as one level so that it doesnt leave a directory hierarchy on the filesystem
671+
// eg tmp dir will be something like: /tmp/TestFoundation-test_name-BE16B2FF-37FA-4F70-8A84-923D1CC2A860
672+
let fname = testBundleName() + "-" + String(functionName[..<idx]) + "-" + NSUUID().uuidString
673+
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(fname)
672674
let fm = FileManager.default
673675
try? fm.removeItem(at: tmpDir)
674676
try fm.createDirectory(at: tmpDir, withIntermediateDirectories: true)

0 commit comments

Comments
 (0)