Skip to content

Commit 4d63509

Browse files
Fix Linux memory and CPU metrics derived from procfs (#46)
### Motivation This package provides some system metrics on Linux derived from procfs. These include CPU and memory metrics. The values returned from `/proc/self/stat` need to be combined with system values. For example, some values are returned in clock ticks or memory pages, so need to be divided appropriately according to the system configuration for clock ticks per second or page size, respectively. Looking at the code, some attempt was made to do this: `_SC_CLK_TCK` and `_SC_PAGESIZE` were used as divisors. However, these values are the values of the _option_ parameter used in the call to `sysconf(3)`, not the results themselves. To illustrate this, here's a Swift program that prints the option name, option value, and the result of `sysconf(optionValue)`: ```swift #if !os(Linux) import Foundation print("error: This tool is only supported on Linux") exit(1) #else import Glibc for (optionName, optionValue) in [ ("_SC_ARG_MAX", _SC_ARG_MAX), ("_SC_CHILD_MAX", _SC_CHILD_MAX), ("_SC_CLK_TCK", _SC_CLK_TCK), ("_SC_NGROUPS_MAX", _SC_NGROUPS_MAX), ("_SC_OPEN_MAX", _SC_OPEN_MAX), ("_SC_STREAM_MAX", _SC_STREAM_MAX), ("_SC_TZNAME_MAX", _SC_TZNAME_MAX), ("_SC_JOB_CONTROL", _SC_JOB_CONTROL), ("_SC_SAVED_IDS", _SC_SAVED_IDS), ("_SC_REALTIME_SIGNALS", _SC_REALTIME_SIGNALS), ("_SC_PRIORITY_SCHEDULING", _SC_PRIORITY_SCHEDULING), ("_SC_TIMERS", _SC_TIMERS), ("_SC_ASYNCHRONOUS_IO", _SC_ASYNCHRONOUS_IO), ("_SC_PRIORITIZED_IO", _SC_PRIORITIZED_IO), ("_SC_SYNCHRONIZED_IO", _SC_SYNCHRONIZED_IO), ("_SC_FSYNC", _SC_FSYNC), ("_SC_MAPPED_FILES", _SC_MAPPED_FILES), ("_SC_MEMLOCK", _SC_MEMLOCK), ("_SC_MEMLOCK_RANGE", _SC_MEMLOCK_RANGE), ("_SC_MEMORY_PROTECTION", _SC_MEMORY_PROTECTION), ("_SC_MESSAGE_PASSING", _SC_MESSAGE_PASSING), ("_SC_SEMAPHORES", _SC_SEMAPHORES), ("_SC_SHARED_MEMORY_OBJECTS", _SC_SHARED_MEMORY_OBJECTS), ("_SC_AIO_LISTIO_MAX", _SC_AIO_LISTIO_MAX), ("_SC_AIO_MAX", _SC_AIO_MAX), ("_SC_AIO_PRIO_DELTA_MAX", _SC_AIO_PRIO_DELTA_MAX), ("_SC_DELAYTIMER_MAX", _SC_DELAYTIMER_MAX), ("_SC_MQ_OPEN_MAX", _SC_MQ_OPEN_MAX), ("_SC_MQ_PRIO_MAX", _SC_MQ_PRIO_MAX), ("_SC_VERSION", _SC_VERSION), ("_SC_PAGESIZE", _SC_PAGESIZE), ("_SC_RTSIG_MAX", _SC_RTSIG_MAX), ] { let actualValue = sysconf(Int32(optionValue)) print("Option name: \(optionName), Option value: \(optionValue), Actual value: \(actualValue)") } #endif ``` ```console % swift sysconf-test/sysconf-test.swift Option name: _SC_ARG_MAX, Option value: 0, Actual value: 2097152 Option name: _SC_CHILD_MAX, Option value: 1, Actual value: 63907 Option name: _SC_CLK_TCK, Option value: 2, Actual value: 100 Option name: _SC_NGROUPS_MAX, Option value: 3, Actual value: 65536 Option name: _SC_OPEN_MAX, Option value: 4, Actual value: 65536 Option name: _SC_STREAM_MAX, Option value: 5, Actual value: 16 Option name: _SC_TZNAME_MAX, Option value: 6, Actual value: -1 Option name: _SC_JOB_CONTROL, Option value: 7, Actual value: 1 Option name: _SC_SAVED_IDS, Option value: 8, Actual value: 1 Option name: _SC_REALTIME_SIGNALS, Option value: 9, Actual value: 200809 Option name: _SC_PRIORITY_SCHEDULING, Option value: 10, Actual value: 200809 Option name: _SC_TIMERS, Option value: 11, Actual value: 200809 Option name: _SC_ASYNCHRONOUS_IO, Option value: 12, Actual value: 200809 Option name: _SC_PRIORITIZED_IO, Option value: 13, Actual value: 200809 Option name: _SC_SYNCHRONIZED_IO, Option value: 14, Actual value: 200809 Option name: _SC_FSYNC, Option value: 15, Actual value: 200809 Option name: _SC_MAPPED_FILES, Option value: 16, Actual value: 200809 Option name: _SC_MEMLOCK, Option value: 17, Actual value: 200809 Option name: _SC_MEMLOCK_RANGE, Option value: 18, Actual value: 200809 Option name: _SC_MEMORY_PROTECTION, Option value: 19, Actual value: 200809 Option name: _SC_MESSAGE_PASSING, Option value: 20, Actual value: 200809 Option name: _SC_SEMAPHORES, Option value: 21, Actual value: 200809 Option name: _SC_SHARED_MEMORY_OBJECTS, Option value: 22, Actual value: 200809 Option name: _SC_AIO_LISTIO_MAX, Option value: 23, Actual value: -1 Option name: _SC_AIO_MAX, Option value: 24, Actual value: -1 Option name: _SC_AIO_PRIO_DELTA_MAX, Option value: 25, Actual value: 20 Option name: _SC_DELAYTIMER_MAX, Option value: 26, Actual value: 2147483647 Option name: _SC_MQ_OPEN_MAX, Option value: 27, Actual value: -1 Option name: _SC_MQ_PRIO_MAX, Option value: 28, Actual value: 32768 Option name: _SC_VERSION, Option value: 29, Actual value: 200809 Option name: _SC_PAGESIZE, Option value: 30, Actual value: 4096 Option name: _SC_RTSIG_MAX, Option value: 31, Actual value: 32 ``` The results for our metrics were off because we were always using 30 for page size and 2 for clock ticks. ### Modifications - Remove the LinuxMain.swift: test discovery is supported on Linux for all currently supported Swift versions. - Update the internal documentation regarding the use of `proc_pid_stat(5)`. - Use correct value for CPU metrics, obtained from `sysconf(_SC_CLK_TCK)`, not just `_SC_CLK_TCK`. - Use correct value for memory metrics, obtained from `sysconf(_SC_PAGESIZE)`, not just `_SC_PAGESIZE`. - Write tests for both CPU seconds and RSS memory. ### Result Fixes #40.
1 parent d269598 commit 4d63509

File tree

4 files changed

+139
-70
lines changed

4 files changed

+139
-70
lines changed

Sources/SystemMetrics/SystemMetrics.swift

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,67 @@ public enum SystemMetrics {
364364
private static let cpuUsageCalculator = CPUUsageCalculator()
365365

366366
internal static func linuxSystemMetrics() -> SystemMetrics.Data? {
367+
/// The current implementation below reads /proc/self/stat. Then,
368+
/// presumably to accommodate whitespace in the `comm` field
369+
/// without dealing with null-terminated C strings, it splits on the
370+
/// closing parenthesis surrounding the value. It then splits the
371+
/// remaining string by space, meaning the first element in the
372+
/// resulting array, at index 0, refers to the the third field.
373+
///
374+
/// Note that the man page documents fields starting at index 1.
375+
///
376+
/// ````
377+
/// proc_pid_stat(5) File Formats Manual proc_pid_stat(5)
378+
///
379+
/// ...
380+
///
381+
/// (1) pid %d
382+
/// The process ID.
383+
///
384+
/// (2) comm %s
385+
/// The filename of the executable, in parentheses.
386+
/// Strings longer than TASK_COMM_LEN (16) characters
387+
/// (including the terminating null byte) are silently
388+
/// truncated. This is visible whether or not the
389+
/// executable is swapped out.
390+
/// ...
391+
///
392+
/// (14) utime %lu
393+
/// Amount of time that this process has been scheduled
394+
/// in user mode, measured in clock ticks (divide by
395+
/// sysconf(_SC_CLK_TCK)). This includes guest time,
396+
/// guest_time (time spent running a virtual CPU, see
397+
/// below), so that applications that are not aware of
398+
/// the guest time field do not lose that time from
399+
/// their calculations.
400+
///
401+
/// (15) stime %lu
402+
/// Amount of time that this process has been scheduled
403+
/// in kernel mode, measured in clock ticks (divide by
404+
/// sysconf(_SC_CLK_TCK)).
405+
///
406+
/// ...
407+
///
408+
/// (22) starttime %llu
409+
/// The time the process started after system boot.
410+
/// Before Linux 2.6, this value was expressed in
411+
/// jiffies. Since Linux 2.6, the value is expressed in
412+
/// clock ticks (divide by sysconf(_SC_CLK_TCK)).
413+
///
414+
/// The format for this field was %lu before Linux 2.6.
415+
///
416+
/// (23) vsize %lu
417+
/// Virtual memory size in bytes.
418+
/// The format for this field was %lu before Linux 2.6.
419+
///
420+
/// (24) rss %ld
421+
/// Resident Set Size: number of pages the process has
422+
/// in real memory. This is just the pages which count
423+
/// toward text, data, or stack space. This does not
424+
/// include pages which have not been demand-loaded in,
425+
/// or which are swapped out. This value is inaccurate;
426+
/// see /proc/pid/statm below.
427+
/// ```
367428
enum StatIndices {
368429
static let virtualMemoryBytes = 20
369430
static let residentMemoryBytes = 21
@@ -372,7 +433,14 @@ public enum SystemMetrics {
372433
static let stimeTicks = 12
373434
}
374435

375-
let ticks = _SC_CLK_TCK
436+
/// Some of the metrics from procfs need to be combined with system
437+
/// values, which we obtain from sysconf(3). These values do not change
438+
/// during the lifetime of the process so we define them as static
439+
/// members here.
440+
enum SystemConfiguration {
441+
static let clockTicksPerSecond = sysconf(Int32(_SC_CLK_TCK))
442+
static let pageByteCount = sysconf(Int32(_SC_PAGESIZE))
443+
}
376444

377445
let statFile = CFile("/proc/self/stat")
378446
statFile.open()
@@ -406,10 +474,11 @@ public enum SystemMetrics {
406474
let utimeTicks = Int(stats[safe: StatIndices.utimeTicks]),
407475
let stimeTicks = Int(stats[safe: StatIndices.stimeTicks])
408476
else { return nil }
409-
let residentMemoryBytes = rss * _SC_PAGESIZE
410-
let processStartTimeInSeconds = startTimeTicks / ticks
477+
478+
let residentMemoryBytes = rss * SystemConfiguration.pageByteCount
479+
let processStartTimeInSeconds = startTimeTicks / SystemConfiguration.clockTicksPerSecond
411480
let cpuTicks = utimeTicks + stimeTicks
412-
let cpuSeconds = cpuTicks / ticks
481+
let cpuSeconds = cpuTicks / SystemConfiguration.clockTicksPerSecond
413482

414483
guard let systemStartTimeInSecondsSinceEpoch = SystemMetrics.systemStartTimeInSecondsSinceEpoch else {
415484
return nil
@@ -422,7 +491,7 @@ public enum SystemMetrics {
422491
let uptimeSeconds = Float(uptimeString),
423492
uptimeSeconds.isFinite
424493
else { return nil }
425-
let uptimeTicks = Int(ceilf(uptimeSeconds)) * ticks
494+
let uptimeTicks = Int(ceilf(uptimeSeconds)) * SystemConfiguration.clockTicksPerSecond
426495
cpuUsage = SystemMetrics.cpuUsageCalculator.getUsagePercentage(
427496
ticksSinceSystemBoot: uptimeTicks,
428497
cpuTicks: cpuTicks

Tests/LinuxMain.swift

Lines changed: 0 additions & 31 deletions
This file was deleted.

Tests/SystemMetricsTests/SystemMetricsTests+XCTest.swift

Lines changed: 0 additions & 34 deletions
This file was deleted.

Tests/SystemMetricsTests/SystemMetricsTests.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,69 @@ class SystemMetricsTest: XCTestCase {
109109
throw XCTSkip()
110110
#endif
111111
}
112+
113+
func testLinuxResidentMemoryBytes() throws {
114+
#if os(Linux)
115+
116+
let pageByteCount = sysconf(Int32(_SC_PAGESIZE))
117+
let allocationSize = 10_000 * pageByteCount
118+
119+
let warmups = 20
120+
for _ in 0..<warmups {
121+
let bytes = UnsafeMutableRawBufferPointer.allocate(byteCount: allocationSize, alignment: 1)
122+
defer { bytes.deallocate() }
123+
bytes.initializeMemory(as: UInt8.self, repeating: .zero)
124+
}
125+
126+
guard let startResidentMemoryBytes = SystemMetrics.linuxSystemMetrics()?.residentMemoryBytes else {
127+
XCTFail("Could not get resident memory usage.")
128+
return
129+
}
130+
131+
let bytes = UnsafeMutableRawBufferPointer.allocate(byteCount: allocationSize, alignment: 1)
132+
defer { bytes.deallocate() }
133+
bytes.initializeMemory(as: UInt8.self, repeating: .zero)
134+
135+
guard let residentMemoryBytes = SystemMetrics.linuxSystemMetrics()?.residentMemoryBytes else {
136+
XCTFail("Could not get resident memory usage.")
137+
return
138+
}
139+
140+
/// According to the man page for proc_pid_stat(5) the value is
141+
/// advertised as inaccurate. It refers to proc_pid_statm(5), which
142+
/// itself states:
143+
///
144+
/// Some of these values are inaccurate because of a kernel-
145+
/// internal scalability optimization. If accurate values are
146+
/// required, use /proc/pid/smaps or /proc/pid/smaps_rollup
147+
/// instead, which are much slower but provide accurate,
148+
/// detailed information.
149+
///
150+
/// Deferring discussion on whether we should extend this package to
151+
/// produce these slower-to-retrieve, more-accurate values, we check
152+
/// that the RSS value is within 1% of the expected allocation increase.
153+
XCTAssertEqual(residentMemoryBytes - startResidentMemoryBytes, allocationSize, accuracy: allocationSize / 100)
154+
155+
#else
156+
throw XCTSkip()
157+
#endif
158+
}
159+
160+
func testLinuxCPUSeconds() throws {
161+
#if os(Linux)
162+
163+
let bytes = Array(repeating: UInt8.zero, count: 10)
164+
var hasher = Hasher()
165+
166+
let startTime = Date()
167+
while Date().timeIntervalSince(startTime) < 1 {
168+
bytes.hash(into: &hasher)
169+
}
170+
171+
XCTAssertEqual(SystemMetrics.linuxSystemMetrics()?.cpuSeconds, 1)
172+
173+
#else
174+
throw XCTSkip()
175+
#endif
176+
}
112177
}

0 commit comments

Comments
 (0)