FPS監視
iOSデバイスのスクリーンは、一般的に60Hzのリフレッシュレートを持ちます。これは1秒間に60回のVSyncシグナルが送られ、各フレームの表示間隔は約16.67ミリ秒(1000ms / 60)であることを意味します。もし、この16.67ミリ秒のウィンドウ内で次のフレームのデータが準備できなければ、フレームドロップ(カクつき)が発生します。
CADisplayLinkクラスは、ディスプレイのリフレッシュレートと同期してコールバックを呼び出すことができるため、FPS(Frames Per Second)の監視に最適です。一定時間内のフレーム数をカウントし、その時間で割ることでFPSを計算できます。
import UIKit
class PerformanceMonitorView: UILabel {
private var displayLink: CADisplayLink!
private var frameCounter: Int = 0
private var lastTimestamp: CFTimeInterval = 0
private var currentFPS: Double = 0
override init(frame: CGRect) {
super.init(frame: frame)
self.setupUI()
self.setupDisplayLink()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupUI()
self.setupDisplayLink()
}
private func setupUI() {
self.textColor = .white
self.textAlignment = .center
self.font = UIFont(name: "Menlo", size: 12)
self.backgroundColor = .lightGray
}
private func setupDisplayLink() {
displayLink = CADisplayLink(target: self, selector: #selector(updateDisplay(_:)))
displayLink.add(to: .main, forMode: .common)
}
@objc private func updateDisplay(_ link: CADisplayLink) {
// 初回のタイムスタンプを記録
if lastTimestamp == 0 {
lastTimestamp = link.timestamp
return
}
frameCounter += 1
let timeDifference = link.timestamp - lastTimestamp
// 1秒間の経過を確認
guard timeDifference >= 1.0 else {
return
}
lastTimestamp = link.timestamp
currentFPS = Double(frameCounter) / timeDifference
frameCounter = 0
let fpsText = String(format: "%.1f FPS", currentFPS)
let statusColor = self.determineStatusColor(for: currentFPS)
let attributedString = NSMutableAttributedString(string: fpsText)
attributedString.addAttribute(.foregroundColor, value: statusColor, range: NSRange(location: 0, length: fpsText.count - 3))
attributedString.addAttribute(.foregroundColor, value: .white, range: NSRange(location: fpsText.count - 3, length: 3))
DispatchQueue.main.async {
self.attributedText = attributedString
}
}
private func determineStatusColor(for fps: Double) -> UIColor {
if fps >= 55.0 {
return .green // スムーズ
} else if fps >= 50.0 {
return .yellow // 良好
} else {
return .red // カクつき
}
}
deinit {
displayLink.invalidate()
}
}
RunLoop監視
主スレッドのRunLoopの状態を監視することで、長時間実行されている処理(ブロッキング)を検出できます。特に、`kCFRunLoopBeforeSources`(ソースの処理開始前)と`kCFRunLoopAfterWaiting`(待機後)の状態の遷移時間を測定します。これらの状態間の遷移が長時間かかると、UIの応答性が低下している可能性があります。
この監視は、通常サブスレッドで行われます。CFRunLoopObserverを利用して、RunLoopの各アクティビティを監視し、特定の状態遷移の時間を計測します。信号(semaphore)を使用して、状態の変化を待機し、タイムアウトを検出します。
#import "RunLoopStallDetector.h"
@interface RunLoopStallDetector ()
@property (nonatomic, strong) dispatch_semaphore_t activitySemaphore;
@property (nonatomic, assign) NSUInteger stallCounter;
@property (nonatomic, assign) CFRunLoopActivity lastActivity;
@end
@implementation RunLoopStallDetector
+ (instancetype)sharedInstance {
static RunLoopStallDetector *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (void)startMonitoring {
[self registerRunLoopObserver];
[self startStallDetectionLoop];
}
- (void)registerRunLoopObserver {
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
// 全てのアクティビティを監視
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
RunLoopStallDetectorObserverCallback,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);
}
- (void)startStallDetectionLoop {
self.activitySemaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (YES) {
// 1秒間待機。シグナルが来なければタイムアウト
long waitResult = dispatch_semaphore_wait(self.activitySemaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (waitResult != 0) {
// タイムアウトが発生
if (self.lastActivity == kCFRunLoopBeforeSources || self.lastActivity == kCFRunLoopAfterWaiting) {
self.stallCounter++;
if (self.stallCounter > 2) {
NSLog(@"警告: 連続して3回以上のカクつきが検出されました。");
self.stallCounter = 0; // カウンターをリセット
}
}
} else {
// 正常な状態遷移
self.stallCounter = 0;
}
}
});
}
static void RunLoopStallDetectorObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunLoopStallDetector *detector = (__bridge RunLoopStallDetector *)info;
detector.lastActivity = activity;
// シグナルを送信して待機を解除
dispatch_semaphore_signal(detector.activitySemaphore);
}
- (void)dealloc {
CFRunLoopObserverInvalidate(self.runLoopObserver);
CFRelease(self.runLoopObserver);
}
@end