22//
33// This source file is part of the Swift Metrics API open source project
44//
5- // Copyright (c) 2018-2020 Apple Inc. and the Swift Metrics API project authors
5+ // Copyright (c) 2018-2023 Apple Inc. and the Swift Metrics API project authors
66// Licensed under Apache License v2.0
77//
88// See LICENSE.txt for license information
@@ -69,6 +69,7 @@ extension MetricsSystem {
6969 Gauge ( label: self . labels. label ( for: \. cpuSecondsTotal) , dimensions: self . dimensions) . record ( metrics. cpuSeconds)
7070 Gauge ( label: self . labels. label ( for: \. maxFileDescriptors) , dimensions: self . dimensions) . record ( metrics. maxFileDescriptors)
7171 Gauge ( label: self . labels. label ( for: \. openFileDescriptors) , dimensions: self . dimensions) . record ( metrics. openFileDescriptors)
72+ Gauge ( label: self . labels. label ( for: \. cpuUsage) , dimensions: self . dimensions) . record ( metrics. cpuUsage)
7273 } ) )
7374
7475 self . timer. schedule ( deadline: . now( ) + self . timeInterval, repeating: self . timeInterval)
@@ -145,6 +146,8 @@ public enum SystemMetrics {
145146 let maxFileDescriptors : String
146147 /// Number of open file descriptors.
147148 let openFileDescriptors : String
149+ /// CPU usage percentage.
150+ let cpuUsage : String
148151
149152 /// Create a new `Labels` instance.
150153 ///
@@ -156,14 +159,25 @@ public enum SystemMetrics {
156159 /// - cpuSecondsTotal: Total user and system CPU time spent in seconds.
157160 /// - maxFds: Maximum number of open file descriptors.
158161 /// - openFds: Number of open file descriptors.
159- public init ( prefix: String , virtualMemoryBytes: String , residentMemoryBytes: String , startTimeSeconds: String , cpuSecondsTotal: String , maxFds: String , openFds: String ) {
162+ /// - cpuUsage: Total CPU usage percentage.
163+ public init (
164+ prefix: String ,
165+ virtualMemoryBytes: String ,
166+ residentMemoryBytes: String ,
167+ startTimeSeconds: String ,
168+ cpuSecondsTotal: String ,
169+ maxFds: String ,
170+ openFds: String ,
171+ cpuUsage: String
172+ ) {
160173 self . prefix = prefix
161174 self . virtualMemoryBytes = virtualMemoryBytes
162175 self . residentMemoryBytes = residentMemoryBytes
163176 self . startTimeSeconds = startTimeSeconds
164177 self . cpuSecondsTotal = cpuSecondsTotal
165178 self . maxFileDescriptors = maxFds
166179 self . openFileDescriptors = openFds
180+ self . cpuUsage = cpuUsage
167181 }
168182
169183 func label( for keyPath: KeyPath < Labels , String > ) -> String {
@@ -173,7 +187,7 @@ public enum SystemMetrics {
173187
174188 /// System Metric data.
175189 ///
176- /// The current list of metrics exposed is taken from the Prometheus Client Library Guidelines
190+ /// The current list of metrics exposed is a superset of the Prometheus Client Library Guidelines:
177191 /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors
178192 public struct Data {
179193 /// Virtual memory size in bytes.
@@ -188,82 +202,121 @@ public enum SystemMetrics {
188202 var maxFileDescriptors : Int
189203 /// Number of open file descriptors.
190204 var openFileDescriptors : Int
205+ /// CPU usage percentage.
206+ var cpuUsage : Double
191207 }
192208
193209 #if os(Linux)
194- internal static func linuxSystemMetrics( ) -> SystemMetrics . Data ? {
195- /// Minimal file reading implementation so we don't have to depend on Foundation.
196- /// Designed only for the narrow use case of this library, reading `/proc/self/stat`.
197- final class CFile {
198- let path : String
210+ /// Minimal file reading implementation so we don't have to depend on Foundation.
211+ /// Designed only for the narrow use case of this library.
212+ final class CFile {
213+ let path : String
199214
200- private var file : UnsafeMutablePointer < FILE > ?
215+ private var file : UnsafeMutablePointer < FILE > ?
201216
202- init ( _ path: String ) {
203- self . path = path
204- }
217+ init ( _ path: String ) {
218+ self . path = path
219+ }
205220
206- deinit {
207- assert ( self . file == nil )
208- }
221+ deinit {
222+ assert ( self . file == nil )
223+ }
209224
210- func open( ) {
211- guard let f = fopen ( path, " r " ) else {
212- return
213- }
214- self . file = f
225+ func open( ) {
226+ guard let f = fopen ( path, " r " ) else {
227+ return
215228 }
229+ self . file = f
230+ }
216231
217- func close( ) {
218- if let f = self . file {
219- self . file = nil
220- let success = fclose ( f) == 0
221- assert ( success)
222- }
232+ func close( ) {
233+ if let f = self . file {
234+ self . file = nil
235+ let success = fclose ( f) == 0
236+ assert ( success)
223237 }
238+ }
224239
225- func readLine( ) -> String ? {
226- guard let f = self . file else {
227- return nil
228- }
229- var buff = [ CChar] ( repeating: 0 , count: 1024 )
230- let hasNewLine = buff. withUnsafeMutableBufferPointer { ptr -> Bool in
231- guard fgets ( ptr. baseAddress, Int32 ( ptr. count) , f) != nil else {
232- if feof ( f) != 0 {
233- return false
234- } else {
235- preconditionFailure ( " Error reading line " )
236- }
240+ func readLine( ) -> String ? {
241+ guard let f = self . file else {
242+ return nil
243+ }
244+ var buff = [ CChar] ( repeating: 0 , count: 1024 )
245+ let hasNewLine = buff. withUnsafeMutableBufferPointer { ptr -> Bool in
246+ guard fgets ( ptr. baseAddress, Int32 ( ptr. count) , f) != nil else {
247+ if feof ( f) != 0 {
248+ return false
249+ } else {
250+ preconditionFailure ( " Error reading line " )
237251 }
238- return true
239- }
240- if !hasNewLine {
241- return nil
242252 }
243- return String ( cString : buff )
253+ return true
244254 }
255+ if !hasNewLine {
256+ return nil
257+ }
258+ return String ( cString: buff)
259+ }
245260
246- func readFull( ) -> String {
247- var s = " "
248- func loop( ) -> String {
249- if let l = readLine ( ) {
250- s += l
251- return loop ( )
252- }
253- return s
261+ func readFull( ) -> String {
262+ var s = " "
263+ func loop( ) -> String {
264+ if let l = readLine ( ) {
265+ s += l
266+ return loop ( )
254267 }
255- return loop ( )
268+ return s
256269 }
270+ return loop ( )
257271 }
272+ }
258273
259- let ticks = _SC_CLK_TCK
274+ /// A type that can calculate CPU usage for a given process.
275+ ///
276+ /// CPU usage is calculated as the number of CPU ticks used by this process between measurements.
277+ /// - Note: the first measurement will be calculated since the process' start time, since there's no
278+ /// previous measurement to take as reference.
279+ private struct CPUUsageCalculator {
280+ /// The number of ticks after system boot that the last CPU usage stat was taken.
281+ private var previousTicksSinceSystemBoot : Int = 0
282+ /// The number of ticks the process actively used the CPU, as of the previous CPU usage measurement.
283+ private var previousCPUTicks : Int = 0
284+
285+ mutating func getUsagePercentage( ticksSinceSystemBoot: Int , cpuTicks: Int ) -> Double {
286+ defer {
287+ self . previousTicksSinceSystemBoot = ticksSinceSystemBoot
288+ self . previousCPUTicks = cpuTicks
289+ }
290+ let ticksBetweenMeasurements = ticksSinceSystemBoot - self . previousTicksSinceSystemBoot
291+ let cpuTicksBetweenMeasurements = cpuTicks - self . previousCPUTicks
292+ return Double ( cpuTicksBetweenMeasurements) * 100 / Double( ticksBetweenMeasurements)
293+ }
294+ }
260295
261- let file = CFile ( " /proc/self/stat " )
262- file. open ( )
296+ private static let systemStartTimeInSecondsSinceEpoch : Int ? = {
297+ let systemStatFile = CFile ( " /proc/stat " )
298+ systemStatFile. open ( )
263299 defer {
264- file . close ( )
300+ systemStatFile . close ( )
265301 }
302+ while let line = systemStatFile. readLine ( ) {
303+ if line. starts ( with: " btime " ) ,
304+ let systemUptimeInSecondsSinceEpochString = line
305+ . split ( separator: " " )
306+ . last?
307+ . split ( separator: " \n " )
308+ . first,
309+ let systemUptimeInSecondsSinceEpoch = Int ( systemUptimeInSecondsSinceEpochString)
310+ {
311+ return systemUptimeInSecondsSinceEpoch
312+ }
313+ }
314+ return nil
315+ } ( )
266316
317+ private static var cpuUsageCalculator = CPUUsageCalculator ( )
318+
319+ internal static func linuxSystemMetrics( ) -> SystemMetrics . Data ? {
267320 enum StatIndices {
268321 static let virtualMemoryBytes = 20
269322 static let residentMemoryBytes = 21
@@ -272,8 +325,26 @@ public enum SystemMetrics {
272325 static let stimeTicks = 12
273326 }
274327
328+ let ticks = _SC_CLK_TCK
329+
330+ let statFile = CFile ( " /proc/self/stat " )
331+ statFile. open ( )
332+ defer {
333+ statFile. close ( )
334+ }
335+
336+ let uptimeFile = CFile ( " /proc/uptime " )
337+ uptimeFile. open ( )
338+ defer {
339+ uptimeFile. close ( )
340+ }
341+
342+ // Read both files as close as possible to each other to get an accurate CPU usage metric.
343+ let statFileContents = statFile. readFull ( )
344+ let uptimeFileContents = uptimeFile. readFull ( )
345+
275346 guard
276- let statString = file . readFull ( )
347+ let statString = statFileContents
277348 . split ( separator: " ) " )
278349 . last
279350 else { return nil }
@@ -288,11 +359,25 @@ public enum SystemMetrics {
288359 let stimeTicks = Int ( stats [ safe: StatIndices . stimeTicks] )
289360 else { return nil }
290361 let residentMemoryBytes = rss * _SC_PAGESIZE
291- let startTimeSeconds = startTimeTicks / ticks
292- let cpuSeconds = ( utimeTicks / ticks) + ( stimeTicks / ticks)
362+ let processStartTimeInSeconds = startTimeTicks / ticks
363+ let cpuTicks = utimeTicks + stimeTicks
364+ let cpuSeconds = cpuTicks / ticks
293365
294- var _rlim = rlimit ( )
366+ guard let systemStartTimeInSecondsSinceEpoch = SystemMetrics . systemStartTimeInSecondsSinceEpoch else {
367+ return nil
368+ }
369+ let startTimeInSecondsSinceEpoch = systemStartTimeInSecondsSinceEpoch + processStartTimeInSeconds
370+
371+ var cpuUsage : Double = 0
372+ if cpuTicks > 0 {
373+ guard let uptimeString = uptimeFileContents. split ( separator: " " ) . first,
374+ let uptimeSeconds = Float ( uptimeString)
375+ else { return nil }
376+ let uptimeTicks = Int ( ceilf ( uptimeSeconds) ) * ticks
377+ cpuUsage = SystemMetrics . cpuUsageCalculator. getUsagePercentage ( ticksSinceSystemBoot: uptimeTicks, cpuTicks: cpuTicks)
378+ }
295379
380+ var _rlim = rlimit ( )
296381 guard withUnsafeMutablePointer ( to: & _rlim, { ptr in
297382 getrlimit ( __rlimit_resource_t ( RLIMIT_NOFILE . rawValue) , ptr) == 0
298383 } ) else { return nil }
@@ -309,10 +394,11 @@ public enum SystemMetrics {
309394 return . init(
310395 virtualMemoryBytes: virtualMemoryBytes,
311396 residentMemoryBytes: residentMemoryBytes,
312- startTimeSeconds: startTimeSeconds ,
397+ startTimeSeconds: startTimeInSecondsSinceEpoch ,
313398 cpuSeconds: cpuSeconds,
314399 maxFileDescriptors: maxFileDescriptors,
315- openFileDescriptors: openFileDescriptors
400+ openFileDescriptors: openFileDescriptors,
401+ cpuUsage: cpuUsage
316402 )
317403 }
318404
0 commit comments