From fceb2232de4e258496c3d8a593ea12520a4b5557 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Wed, 20 Mar 2024 17:56:13 +0000 Subject: [PATCH 1/3] Server Stats Collection Motivation: In order to implement the 'runServer' RPC on the WorkerService we need to capture a snapshot with the resorces usage for the server and compute the difference between 2 of these snapshots. Modifications: - Created the ServerStats struct that contains all stats needed in the QPS benchmarking from the server - Implemented the init() that collects all these stats - Implemented the function that computes the differences between 2 snapshots of ServerStats Result: We will be able to implement the `runServer` RPC. --- Package.swift | 6 +- Sources/performance-worker/ServerStats.swift | 131 +++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 Sources/performance-worker/ServerStats.swift diff --git a/Package.swift b/Package.swift index 83d52b749..699ff4baa 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.58.0" + from: "2.64.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", @@ -132,6 +132,7 @@ extension Target.Dependency { package: "swift-nio-transport-services" ) static let nioTestUtils: Self = .product(name: "NIOTestUtils", package: "swift-nio") + static let nioFileSystem: Self = .product(name: "_NIOFileSystem", package: "swift-nio") static let logging: Self = .product(name: "Logging", package: "swift-log") static let protobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") static let protobufPluginLibrary: Self = .product( @@ -251,7 +252,8 @@ extension Target { dependencies: [ .grpcCore, .grpcProtobuf, - .nioCore + .nioCore, + .nioFileSystem ] ) diff --git a/Sources/performance-worker/ServerStats.swift b/Sources/performance-worker/ServerStats.swift new file mode 100644 index 000000000..172a35464 --- /dev/null +++ b/Sources/performance-worker/ServerStats.swift @@ -0,0 +1,131 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Dispatch +import NIOCore +import NIOFileSystem + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#else +let badOS = { fatalError("unsupported OS") }() +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF +#elseif os(Linux) || os(FreeBSD) || os(Android) +private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue +#endif + +/// Current server stats. +internal struct ServerStats: Sendable { + let time: Double + let userTime: Double + let systemTime: Double + let totalCpuTime: UInt64 + let idleCpuTime: UInt64 + + init( + time: Double, + userTime: Double, + systemTime: Double, + totalCpuTime: UInt64, + idleCPuTime: UInt64 + ) { + self.time = time + self.userTime = userTime + self.systemTime = systemTime + self.totalCpuTime = totalCpuTime + self.idleCpuTime = idleCPuTime + } + + init() async throws { + self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 + var usage = rusage() + if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { + // Adding the seconds with the microseconds transformed into seconds to get the + // real number of seconds as a `Double`. + self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 + self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 + } else { + self.userTime = 0 + self.systemTime = 0 + } + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + let (totalCpuTime, idleCpuTime) = try await getTotalAndIdleCpuTime() + self.totalCpuTime = totalCpuTime + self.idleCpuTime = idleCpuTime + } else { + self.idleCpuTime = 0 + self.totalCpuTime = 0 + } + } +} + +internal func changeInStats(initialStats: ServerStats, currentStats: ServerStats) -> ServerStats { + return ServerStats( + time: currentStats.time - initialStats.time, + userTime: currentStats.userTime - initialStats.userTime, + systemTime: currentStats.systemTime - initialStats.systemTime, + totalCpuTime: currentStats.totalCpuTime - initialStats.totalCpuTime, + idleCPuTime: currentStats.idleCpuTime - initialStats.idleCpuTime + ) +} + +/// Computes the total and idle CPU time after extracting stats from the first line of '/proc/stat'. +/// +/// The first line in '/proc/stat' file looks as follows: +/// cpu [user] [nice] [system] [idle] [iowait] [irq] [softirq] +/// The totalCpuTime is computed as follows: +/// total = user + nice + system + idle +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private func getTotalAndIdleCpuTime() async throws -> (UInt64, UInt64) { + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) + do { + let contents = try await ByteBuffer( + contentsOf: "/proc/stat", + maximumSizeAllowed: .kilobytes(20) + ) + + guard let index = contents.readableBytesView.firstIndex(where: { $0 == UInt8("\n") }) else { + return (0, 0) + } + + guard let firstLine = contents.getString(at: 0, length: index) else { + return (0, 0) + } + + let lineComponents = firstLine.components(separatedBy: " ") + if lineComponents.count < 5 || lineComponents[0] != "cpu" { + return (0, 0) + } + + let cpuTime: [UInt64] = lineComponents[1 ... 4].compactMap { UInt64($0) } + if cpuTime.count < 4 { + return (0, 0) + } + + let totalCpuTime = cpuTime.reduce(0, +) + return (totalCpuTime, cpuTime[3]) + } catch { + return (0, 0) + } + #else + return (0, 0) + #endif +} From ca8f0f50c6aa908770530d654b7eb16f62a10a03 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Thu, 21 Mar 2024 16:16:45 +0000 Subject: [PATCH 2/3] implemented feedback --- Sources/performance-worker/ServerStats.swift | 110 ++++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/Sources/performance-worker/ServerStats.swift b/Sources/performance-worker/ServerStats.swift index 172a35464..cbcdc525f 100644 --- a/Sources/performance-worker/ServerStats.swift +++ b/Sources/performance-worker/ServerStats.swift @@ -18,40 +18,42 @@ import Dispatch import NIOCore import NIOFileSystem -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if canImport(Darwin) import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) +#elseif canImport(Musl) +import Musl +#elseif canImport(Glibc) import Glibc #else let badOS = { fatalError("unsupported OS") }() #endif -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +#if canImport(Darwin) private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF -#elseif os(Linux) || os(FreeBSD) || os(Android) +#elseif canImport(Musl) || canImport(Glibc) private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue #endif /// Current server stats. internal struct ServerStats: Sendable { - let time: Double - let userTime: Double - let systemTime: Double - let totalCpuTime: UInt64 - let idleCpuTime: UInt64 + var time: Double + var userTime: Double + var systemTime: Double + var totalCPUTime: UInt64 + var idleCPUTime: UInt64 init( time: Double, userTime: Double, systemTime: Double, - totalCpuTime: UInt64, - idleCPuTime: UInt64 + totalCPUTime: UInt64, + idleCPUTime: UInt64 ) { self.time = time self.userTime = userTime self.systemTime = systemTime - self.totalCpuTime = totalCpuTime - self.idleCpuTime = idleCPuTime + self.totalCPUTime = totalCPUTime + self.idleCPUTime = idleCPUTime } init() async throws { @@ -67,65 +69,67 @@ internal struct ServerStats: Sendable { self.systemTime = 0 } if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - let (totalCpuTime, idleCpuTime) = try await getTotalAndIdleCpuTime() - self.totalCpuTime = totalCpuTime - self.idleCpuTime = idleCpuTime + let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime() + self.totalCPUTime = totalCPUTime + self.idleCPUTime = idleCPUTime } else { - self.idleCpuTime = 0 - self.totalCpuTime = 0 + self.idleCPUTime = 0 + self.totalCPUTime = 0 } } -} - -internal func changeInStats(initialStats: ServerStats, currentStats: ServerStats) -> ServerStats { - return ServerStats( - time: currentStats.time - initialStats.time, - userTime: currentStats.userTime - initialStats.userTime, - systemTime: currentStats.systemTime - initialStats.systemTime, - totalCpuTime: currentStats.totalCpuTime - initialStats.totalCpuTime, - idleCPuTime: currentStats.idleCpuTime - initialStats.idleCpuTime - ) -} -/// Computes the total and idle CPU time after extracting stats from the first line of '/proc/stat'. -/// -/// The first line in '/proc/stat' file looks as follows: -/// cpu [user] [nice] [system] [idle] [iowait] [irq] [softirq] -/// The totalCpuTime is computed as follows: -/// total = user + nice + system + idle -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -private func getTotalAndIdleCpuTime() async throws -> (UInt64, UInt64) { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) - do { - let contents = try await ByteBuffer( - contentsOf: "/proc/stat", - maximumSizeAllowed: .kilobytes(20) + internal func difference(to stats: ServerStats) -> ServerStats { + return ServerStats( + time: self.time - stats.time, + userTime: self.userTime - stats.userTime, + systemTime: self.systemTime - stats.systemTime, + totalCPUTime: self.totalCPUTime - stats.totalCPUTime, + idleCPUTime: self.idleCPUTime - stats.idleCPUTime ) + } - guard let index = contents.readableBytesView.firstIndex(where: { $0 == UInt8("\n") }) else { + /// Computes the total and idle CPU time after extracting stats from the first line of '/proc/stat'. + /// + /// The first line in '/proc/stat' file looks as follows: + /// CPU [user] [nice] [system] [idle] [iowait] [irq] [softirq] + /// The totalCPUTime is computed as follows: + /// total = user + nice + system + idle + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + private static func getTotalAndIdleCPUTime() async throws -> ( + totalCPUTime: UInt64, idleCPUTime: UInt64 + ) { + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) + let contents: ByteBuffer + do { + contents = try await ByteBuffer( + contentsOf: "/proc/stat", + maximumSizeAllowed: .kilobytes(20) + ) + } catch { return (0, 0) } - guard let firstLine = contents.getString(at: 0, length: index) else { + let view = contents.readableBytesView + guard let firstNewLineIndex = view.firstIndex(of: UInt8(ascii: "\n")) else { return (0, 0) } + let firstLine = String(buffer: ByteBuffer(view[0 ... firstNewLineIndex])) let lineComponents = firstLine.components(separatedBy: " ") - if lineComponents.count < 5 || lineComponents[0] != "cpu" { + if lineComponents.count < 5 || lineComponents[0] != "CPU" { return (0, 0) } - let cpuTime: [UInt64] = lineComponents[1 ... 4].compactMap { UInt64($0) } - if cpuTime.count < 4 { + let CPUTime: [UInt64] = lineComponents[1 ... 4].compactMap { UInt64($0) } + if CPUTime.count < 4 { return (0, 0) } - let totalCpuTime = cpuTime.reduce(0, +) - return (totalCpuTime, cpuTime[3]) - } catch { + let totalCPUTime = CPUTime.reduce(0, +) + return (totalCPUTime, CPUTime[3]) + + #else return (0, 0) + #endif } - #else - return (0, 0) - #endif } From e8aaa78b1b165bc7d6559a03535df6f73efd544e Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Fri, 22 Mar 2024 09:55:49 +0000 Subject: [PATCH 3/3] bubbled up availability guard --- Sources/performance-worker/ServerStats.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/performance-worker/ServerStats.swift b/Sources/performance-worker/ServerStats.swift index cbcdc525f..44d7c8b50 100644 --- a/Sources/performance-worker/ServerStats.swift +++ b/Sources/performance-worker/ServerStats.swift @@ -35,6 +35,7 @@ private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue #endif /// Current server stats. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) internal struct ServerStats: Sendable { var time: Double var userTime: Double @@ -68,14 +69,9 @@ internal struct ServerStats: Sendable { self.userTime = 0 self.systemTime = 0 } - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime() - self.totalCPUTime = totalCPUTime - self.idleCPUTime = idleCPUTime - } else { - self.idleCPUTime = 0 - self.totalCPUTime = 0 - } + let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime() + self.totalCPUTime = totalCPUTime + self.idleCPUTime = idleCPUTime } internal func difference(to stats: ServerStats) -> ServerStats { @@ -94,7 +90,6 @@ internal struct ServerStats: Sendable { /// CPU [user] [nice] [system] [idle] [iowait] [irq] [softirq] /// The totalCPUTime is computed as follows: /// total = user + nice + system + idle - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private static func getTotalAndIdleCPUTime() async throws -> ( totalCPUTime: UInt64, idleCPUTime: UInt64 ) {