SwiftでアプリのCPU使用率とメモリ使用量を取得する


はじめに

Debug時にXCodeからCustomFlagなどをつけてシステム情報を取得する方法でCPUやメモリの状態をプロファイリングするライブラリはObjective-Cから存在しました。

もちろんDebugビルドに限定されないシステム情報の取得方法もありますが、調べてみるとほとんどのものがObjective-Cで書かれていました。
公式のAppleDeveloperフォーラムでも

SwiftでもできなくないがObjective-Cでやる方のが懸命かもしれない(意訳)

という回答もあります。
しかし、Swiftでできない or やるメリットが全くない訳ではありません。個人的に低レイヤな実装においても以下のようなメリットがあると思います。

  • Optionalの恩恵を受けられる
  • 可読性の向上
  • コードの共有が容易になる(メンテナンスの属人化を防ぎやすい)

そこで今回はSwiftでCPU使用率とメモリ使用量の取得方法を書いてみました。(おまけとしてディスク使用量についても)

動作確認環境

XCode8.3.3
Swift3.1

CPU使用率

// 必須
import Foundation

// CPU使用率を0%~100%で取得
private func getCPUUsage() -> Float {
    // カーネル処理の結果
    var result: Int32
    var threadList = UnsafeMutablePointer<UInt32>.allocate(capacity: 1)
    var threadCount = UInt32(MemoryLayout<mach_task_basic_info_data_t>.size / MemoryLayout<natural_t>.size)
    var threadInfo = thread_basic_info()

    // スレッド情報を取得
    result = withUnsafeMutablePointer(to: &threadList) {
        $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) {
            task_threads(mach_task_self_, $0, &threadCount)
        }
    }

    if result != KERN_SUCCESS { return 0 }

    // 各スレッドからCPU使用率を算出し合計を全体のCPU使用率とする
    return (0 ..< Int(threadCount))
        // スレッドのCPU使用率を取得
        .flatMap { index -> Float? in
            var threadInfoCount = UInt32(THREAD_INFO_MAX)
            result = withUnsafeMutablePointer(to: &threadInfo) {
                $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                    thread_info(threadList[index], UInt32(THREAD_BASIC_INFO), $0, &threadInfoCount)
                }
            }
            // スレッド情報が取れない = 該当スレッドのCPU使用率を0とみなす(基本nilが返ることはない)
            if result != KERN_SUCCESS { return nil }
            let isIdle = threadInfo.flags == TH_FLAGS_IDLE
            // CPU使用率がスケール調整済みのため`TH_USAGE_SCALE`で除算し戻す
            return !isIdle ? (Float(threadInfo.cpu_usage) / Float(TH_USAGE_SCALE)) * 100 : nil
        }
        // 合計算出
        .reduce(0, +)
}

Float(threadInfo.cpu_usage) / Float(TH_USAGE_SCALE)の部分は値の精度を確保するための工夫が施されているので除算する必要があります。
この式をCPU使用率 / スケールと置くと、具体的には以下のような処理が事前にされているためです。

CPU使用率 = 0.216 -> 21.6%という値がある場合に、スケール係数はCPU使用率が整数になるような10の累乗が取得されます。そのため、この場合スケール係数は1000となります。(0.216 * 1000 = 216)
よって、これらスケール係数は値の精度によって係数値が異なるためスレッド毎に除算に使用する必要があります。

メモリ使用量

// 必須
import Foundation

// 使用者が単位を把握できるようにするため
typealias MegaByte = UInt64

// 引数にenumで任意の単位を指定できるのが好ましい e.g. unit = .auto (デフォルト引数)
func getMemoryUsed() -> MegaByte? {
    // タスク情報を取得
    var info = mach_task_basic_info()
    // `info`の値からその型に必要なメモリを取得
    var count = UInt32(MemoryLayout.size(ofValue: info) / MemoryLayout<integer_t>.size)
    let result = withUnsafeMutablePointer(to: &info) {
        task_info(mach_task_self_,
                  task_flavor_t(MACH_TASK_BASIC_INFO),
                  // `task_info`の引数にするためにInt32のメモリ配置と解釈させる必要がある
                  $0.withMemoryRebound(to: Int32.self, capacity: 1) { pointer in
                    UnsafeMutablePointer<Int32>(pointer)
                  }, &count)
    }
    // MB表記に変換して返却
    return result == KERN_SUCCESS ? info.resident_size / 1024 / 1024 : nil
}

上記例ではMB単位にしましが、このケースは後述のByteCountFormatterを使用するのがベストです。
returnで使用されているresident_sizeはその時点で使用している実メモリ使用量が取得されます。※XCodeでもメモリ使用量を確認できますが、実メモリ使用量は通常その値より大きくなります。何に消費しているかについてはわかりませんが、初期値が違うだけなので相対的に観測することで問題はなさそうです

ディスク使用量


// 必須(ByteCountFormatterとFileAttributeKeyで使用)
import UIKit

// ディスクスペース種別
enum DiskSpaceType {
    case total
    case free
    case used
}

func getDiskSpace(_ type: DiskSpaceType) -> String {
    // GB,MB,KB表記の文字列に変換
    let byteUnitStringConverted: (Int64) -> String = { size in
        ByteCountFormatter.string(fromByteCount: size,countStyle: ByteCountFormatter.CountStyle.binary)
    }
    switch type {
    case .total:
        // ディスク合計容量
        return byteUnitStringConverted(totalSpace)
    case .free:
        // ディスク空き容量
        return byteUnitStringConverted(freeSpace)
    case .used:
        // ディスク使用量
        return byteUnitStringConverted(usedSpace)
    }
}

var totalSpace: Int64 {
    guard let attributes = systemAttributes,
        let size = (attributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value
        else { return 0 }
    return size
}

var freeSpace: Int64 {
    guard let attributes = systemAttributes,
        let size = (attributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value
        else { return 0 }
    return size
}

var usedSpace: Int64 {
    return totalSpace - freeSpace
}

private var systemAttributes: [FileAttributeKey: Any]? {
    return try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
}

ディスク使用量についてはFileAttributeKeyに紐づく情報から取得することができます。ディスク合計容量についても変動するためcomputed propertyにして置くのが無難かと思います。

まとめ

  • Swiftでもポインターの互換性が高いのでObjective-C相当のレイヤーのコードも実現できる(できないこともある )
  • やっぱりSwiftで書いた方が処理の見通しが良い
  • Swiftらしく書くのは結構難しい..

参考