Skip to content

Frame Rate Management ve 60 FPS Optimizasyonu ​

Modern Mobile Rendering Pipeline ​

Frame Rendering Anatomy ​

Modern mobil cihazlarda her frame'in 16.67ms (60 FPS) süre içinde tamamlanması gerekir. Bu süre aşıldığında "jank" denilen görsel takılmalar oluşur.

Hızlı Karar ​

SemptomÖnce BakDikkat
Scroll takılıyorLayout/draw costGörünür item sayısı azaltılmalı
Animasyon düşüyorMain thread workAğır iş UI thread'den çıkarılmalı
İlk render yavaşAsset decode/layoutLazy load gerekebilir
Düşük cihazda sorunDevice profilingEmulator yeterli değildir

Üretim Kontrol Listesi ​

  • Problem: Hangi frame bütçesi hangi ekranda aşılıyor?
  • Çözüm: Layout, draw, animation, image decode ve background work ayrımı net mi?
  • Trade-off: Görsel detay ve dinamik layout arttıkça frame bütçesi daralır.
  • Hata durumu: Main-thread blocking, GC pause, image decode spike ve layout thrash ele alınmalı.
  • Ölçüm: p95 frame time, dropped frames, jank rate, main-thread time ve memory peak izlenmeli.
  • Güvenlik/maliyet: Performans optimizasyonu erişilebilirliği bozmasın; cihaz laboratuvarı maliyeti planlanmalı.
Frame Timeline (16.67ms @ 60 FPS):
┌─ Input Processing (1-2ms)
├─ Animation Calculations (1-3ms)  
├─ Layout Pass (2-4ms)
├─ Paint/Draw (3-6ms)
├─ Composite (2-4ms)
└─ GPU Rendering (3-5ms)

Android Frame Rate Monitoring ​

Systrace ile Frame Analysis ​

kotlin
// Build.gradle (app level)
android {
    buildTypes {
        debug {
            // Enable GPU profiling
            debuggable true
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
        }
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

Choreographer ile Frame Callback ​

kotlin
class FrameRateMonitor : Choreographer.FrameCallback {
    private var lastFrameTime = 0L
    private val frameTimestamps = mutableListOf<Long>()
    private val maxSamples = 120 // 2 saniye @ 60 FPS
    
    fun startMonitoring() {
        Choreographer.getInstance().postFrameCallback(this)
    }
    
    override fun doFrame(frameTimeNanos: Long) {
        if (lastFrameTime != 0L) {
            val frameDuration = (frameTimeNanos - lastFrameTime) / 1_000_000 // ms
            frameTimestamps.add(frameDuration)
            
            // Dropped frame algılama
            if (frameDuration > 16.67) {
                logDroppedFrame(frameDuration)
            }
            
            // Sliding window ile FPS hesaplama
            if (frameTimestamps.size > maxSamples) {
                frameTimestamps.removeAt(0)
            }
        }
        
        lastFrameTime = frameTimeNanos
        Choreographer.getInstance().postFrameCallback(this)
    }
    
    private fun logDroppedFrame(duration: Double) {
        val droppedFrames = (duration / 16.67).toInt()
        Log.w("FrameRate", "Dropped $droppedFrames frames (${duration}ms)")
        
        // Analytics'e gönder
        FirebaseAnalytics.getInstance(context).logEvent("frame_drop") {
            param("duration_ms", duration)
            param("dropped_count", droppedFrames.toLong())
        }
    }
    
    fun getCurrentFPS(): Double {
        if (frameTimestamps.isEmpty()) return 0.0
        val avgDuration = frameTimestamps.average()
        return 1000.0 / avgDuration
    }
}

GPU Profiler Integration ​

kotlin
class GPUProfiler {
    private val gpuProfiler = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        GpuProfilingApi.getGpuProfiler()
    } else null
    
    fun startGPUProfiling() {
        gpuProfiler?.let { profiler ->
            val profilingRequest = GpuProfilingApi.GpuProfilingRequest.Builder()
                .setSettings(
                    GpuProfilingApi.ClockSettings.Builder()
                        .setGpuClockPointer(GpuProfilingApi.CLOCK_POINTER_TIMESTAMP)
                        .build()
                )
                .build()
                
            profiler.beginProfiling(profilingRequest) { result ->
                when (result.resultCode) {
                    GpuProfilingApi.RESULT_SUCCESS -> {
                        Log.d("GPU", "GPU profiling başlatıldı")
                    }
                    else -> {
                        Log.e("GPU", "GPU profiling başlatılamadı: ${result.resultCode}")
                    }
                }
            }
        }
    }
    
    fun getGPUMetrics(): GpuProfilingApi.GpuProfilingResult? {
        return gpuProfiler?.endProfiling()
    }
}

iOS Core Animation Profiling ​

swift
import QuartzCore

class FrameRateMonitor {
    private var displayLink: CADisplayLink?
    private var lastTimestamp: CFTimeInterval = 0
    private var frameTimestamps: [Double] = []
    private let maxSamples = 120 // 2 seconds @ 60 FPS
    
    func startMonitoring() {
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback))
        displayLink?.preferredFramesPerSecond = 60
        displayLink?.add(to: .main, forMode: .common)
    }
    
    @objc private func displayLinkCallback(displayLink: CADisplayLink) {
        let currentTimestamp = displayLink.timestamp
        
        if lastTimestamp != 0 {
            let frameDuration = (currentTimestamp - lastTimestamp) * 1000 // ms
            frameTimestamps.append(frameDuration)
            
            // Dropped frame detection
            if frameDuration > 16.67 {
                logDroppedFrame(duration: frameDuration)
            }
            
            // Maintain sliding window
            if frameTimestamps.count > maxSamples {
                frameTimestamps.removeFirst()
            }
        }
        
        lastTimestamp = currentTimestamp
    }
    
    private func logDroppedFrame(duration: Double) {
        let droppedFrames = Int(duration / 16.67)
        print("⚠️ Dropped \(droppedFrames) frames (\(String(format: "%.2f", duration))ms)")
        
        // Analytics tracking
        Analytics.logEvent("frame_drop", parameters: [
            "duration_ms": duration,
            "dropped_count": droppedFrames
        ])
    }
    
    func getCurrentFPS() -> Double {
        guard !frameTimestamps.isEmpty else { return 0.0 }
        let avgDuration = frameTimestamps.reduce(0, +) / Double(frameTimestamps.count)
        return 1000.0 / avgDuration
    }
    
    func stopMonitoring() {
        displayLink?.invalidate()
        displayLink = nil
    }
}

Metal Performance HUD ​

swift
import MetalKit

class MetalPerformanceMonitor {
    private var device: MTLDevice
    private var commandQueue: MTLCommandQueue
    private var performanceMetrics: [String: Double] = [:]
    
    init() {
        guard let device = MTLCreateSystemDefaultDevice(),
              let commandQueue = device.makeCommandQueue() else {
            fatalError("Metal not supported")
        }
        
        self.device = device
        self.commandQueue = commandQueue
    }
    
    func measureGPUPerformance<T>(operation: () throws -> T) rethrows -> T {
        let startTime = CACurrentMediaTime()
        let result = try operation()
        let endTime = CACurrentMediaTime()
        
        let gpuTime = (endTime - startTime) * 1000 // ms
        performanceMetrics["last_gpu_operation"] = gpuTime
        
        if gpuTime > 16.67 {
            print("🔥 GPU operation took \(String(format: "%.2f", gpuTime))ms")
        }
        
        return result
    }
    
    func enableMetalHUD() {
        // MetalKit debug layer
        if let metalView = getCurrentMetalView() {
            metalView.preferredFramesPerSecond = 60
            metalView.enableSetNeedsDisplay = false
            metalView.isPaused = false
            
            // Performance HUD
            #if DEBUG
            metalView.device?.makeCommandQueue()?.label = "Performance Monitoring Queue"
            #endif
        }
    }
    
    private func getCurrentMetalView() -> MTKView? {
        // Implementation depends on your app structure
        return nil
    }
}

Cross-Platform Frame Rate Monitoring ​

React Native Performance ​

javascript
// React Native Performance API
import { Performance } from 'react-native-performance';

class RNFrameRateMonitor {
    constructor() {
        this.frameTimestamps = [];
        this.maxSamples = 120;
        this.isMonitoring = false;
    }
    
    startMonitoring() {
        if (this.isMonitoring) return;
        
        this.isMonitoring = true;
        this.scheduleNextFrame();
    }
    
    scheduleNextFrame() {
        if (!this.isMonitoring) return;
        
        requestAnimationFrame((timestamp) => {
            this.recordFrame(timestamp);
            this.scheduleNextFrame();
        });
    }
    
    recordFrame(timestamp) {
        if (this.frameTimestamps.length > 0) {
            const lastFrame = this.frameTimestamps[this.frameTimestamps.length - 1];
            const frameDuration = timestamp - lastFrame;
            
            // 60 FPS = 16.67ms per frame
            if (frameDuration > 16.67) {
                this.logDroppedFrame(frameDuration);
            }
        }
        
        this.frameTimestamps.push(timestamp);
        
        if (this.frameTimestamps.length > this.maxSamples) {
            this.frameTimestamps.shift();
        }
    }
    
    logDroppedFrame(duration) {
        const droppedFrames = Math.floor(duration / 16.67);
        console.warn(`Dropped ${droppedFrames} frames (${duration.toFixed(2)}ms)`);
        
        // Send to analytics
        Performance.mark('frame_drop_start');
        Performance.measure('frame_drop', 'frame_drop_start');
    }
    
    getCurrentFPS() {
        if (this.frameTimestamps.length < 2) return 0;
        
        const totalTime = this.frameTimestamps[this.frameTimestamps.length - 1] - 
                         this.frameTimestamps[0];
        const frameCount = this.frameTimestamps.length - 1;
        
        return (frameCount / totalTime) * 1000;
    }
    
    stopMonitoring() {
        this.isMonitoring = false;
    }
}

// Usage
const monitor = new RNFrameRateMonitor();
monitor.startMonitoring();

// Get FPS periodically
setInterval(() => {
    const fps = monitor.getCurrentFPS();
    console.log(`Current FPS: ${fps.toFixed(1)}`);
}, 1000);

Flutter Frame Timing ​

dart
import 'dart:ui' as ui;
import 'package:flutter/scheduler.dart';

class FlutterFrameRateMonitor {
  List<Duration> _frameTimestamps = [];
  final int _maxSamples = 120;
  bool _isMonitoring = false;
  
  void startMonitoring() {
    if (_isMonitoring) return;
    
    _isMonitoring = true;
    SchedulerBinding.instance.addTimingsCallback(_onReportTimings);
  }
  
  void _onReportTimings(List<FrameTiming> timings) {
    if (!_isMonitoring) return;
    
    for (final timing in timings) {
      final frameDuration = timing.totalSpan;
      _frameTimestamps.add(frameDuration);
      
      // 60 FPS = 16.67ms per frame
      if (frameDuration.inMicroseconds > 16670) {
        _logDroppedFrame(frameDuration);
      }
      
      if (_frameTimestamps.length > _maxSamples) {
        _frameTimestamps.removeAt(0);
      }
    }
  }
  
  void _logDroppedFrame(Duration duration) {
    final durationMs = duration.inMicroseconds / 1000;
    final droppedFrames = (durationMs / 16.67).floor();
    
    print('⚠️ Dropped $droppedFrames frames (${durationMs.toStringAsFixed(2)}ms)');
    
    // Send to Firebase Analytics
    FirebaseAnalytics.instance.logEvent(
      name: 'frame_drop',
      parameters: {
        'duration_ms': durationMs,
        'dropped_count': droppedFrames,
      },
    );
  }
  
  double getCurrentFPS() {
    if (_frameTimestamps.length < 2) return 0.0;
    
    final totalDuration = _frameTimestamps
        .map((d) => d.inMicroseconds)
        .reduce((a, b) => a + b);
    final avgDurationMs = (totalDuration / _frameTimestamps.length) / 1000;
    
    return 1000.0 / avgDurationMs;
  }
  
  void stopMonitoring() {
    _isMonitoring = false;
    SchedulerBinding.instance.removeTimingsCallback(_onReportTimings);
  }
}

Best Practices ve Optimizasyon Teknikleri ​

Render Thread Optimization ​

kotlin
// Android: UI thread'i bloke etmemek için arka plan işlemleri
class BackgroundProcessor {
    private val backgroundExecutor = Executors.newSingleThreadExecutor()
    
    fun processInBackground(heavyTask: () -> Unit, onComplete: () -> Unit) {
        backgroundExecutor.submit {
            heavyTask()
            
            // UI güncellemesi main thread'de
            Handler(Looper.getMainLooper()).post {
                onComplete()
            }
        }
    }
}

Batch Operations ​

swift
// iOS: CATransaction ile animasyonları gruplama
func performBatchedAnimations() {
    CATransaction.begin()
    CATransaction.setAnimationDuration(0.3)
    CATransaction.setCompletionBlock {
        print("All animations completed")
    }
    
    // Multiple layer animations batched together
    layer1.opacity = 0.5
    layer2.transform = CATransform3DMakeScale(1.2, 1.2, 1.0)
    layer3.backgroundColor = UIColor.red.cgColor
    
    CATransaction.commit()
}

Bu kapsamlı frame rate yönetimi rehberi, mobil uygulamalarda 60 FPS hedefini yakalamak ve sürdürmek için gerekli tüm teknikleri ve araçları detaylandırır.

Eren Demir tarafından oluşturulmuştur.