Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -251,7 +252,8 @@ extension Target {
dependencies: [
.grpcCore,
.grpcProtobuf,
.nioCore
.nioCore,
.nioFileSystem
]
)

Expand Down
130 changes: 130 additions & 0 deletions Sources/performance-worker/ServerStats.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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 canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#else
let badOS = { fatalError("unsupported OS") }()
#endif

#if canImport(Darwin)
private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF
#elseif canImport(Musl) || canImport(Glibc)
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
var systemTime: Double
var totalCPUTime: UInt64
var 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
}
let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime()
self.totalCPUTime = totalCPUTime
self.idleCPUTime = idleCPUTime
}

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
)
}

/// 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
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)
}

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" {
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])

#else
return (0, 0)
#endif
}
}