Identificador: PERF-002
Version: 1.0.0
Fecha: 2025-12-08
Autor: SpecQueen Technical Division
Estado: Aprobado
1. Tabla de Contenidos
- Introduccion
- iOS Performance
- Android Performance
- Cross-Platform Metrics
- Testing Methodology
- Optimization Strategies
2. Introduccion
2.1. Proposito
Este documento especifica los requisitos de rendimiento para las aplicaciones moviles iOS y Android de MedTime, incluyendo benchmarks especificos por plataforma, estrategias de optimizacion y metodologia de testing.
2.2. Referencias
| Documento |
Relevancia |
| MOB-IOS-001-arquitectura.md |
Arquitectura iOS |
| MOB-AND-001-arquitectura.md |
Arquitectura Android |
| PERF-001-benchmarks.md |
SLAs globales |
| 04-seguridad-cliente.md |
Cifrado E2E |
2.3. Principios Mobile-First
┌────────────────────────────────────────────────────────────────┐
│ MOBILE PERFORMANCE PRINCIPLES │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. BATTERY IS PRECIOUS │
│ → Minimize background work, batch operations │
│ │
│ 2. NETWORK IS UNRELIABLE │
│ → Offline-first, graceful degradation │
│ │
│ 3. MEMORY IS LIMITED │
│ → Aggressive cleanup, lazy loading │
│ │
│ 4. USER ATTENTION IS SHORT │
│ → Fast launch, responsive UI, progress feedback │
│ │
│ 5. DEVICES VARY WIDELY │
│ → Target mid-range, test on low-end │
│ │
└────────────────────────────────────────────────────────────────┘
3.1. App Lifecycle
| Metrica |
Target |
Warning |
Critical |
Medicion |
| Cold Start |
<2s |
2-3s |
>3s |
Pre-main + post-main |
| Warm Start |
<500ms |
500-800ms |
>1s |
Resume time |
| First Frame |
<1s |
1-1.5s |
>2s |
First contentful paint |
| Interactive |
<2s |
2-3s |
>4s |
User can tap |
// iOS Launch Optimization Checklist
struct LaunchOptimization {
// MARK: - Pre-main Optimizations
static let preMainOptimizations = """
1. Reduce dynamic library count (<6 custom frameworks)
2. Use static linking where possible
3. Minimize +load methods
4. Remove unused code (tree shaking)
5. Use DYLD_PRINT_STATISTICS to measure
"""
// MARK: - Post-main Optimizations
static func optimizedAppDelegate() {
// Defer non-critical initialization
// Phase 1: Critical (blocking)
// - Initialize crash reporting
// - Setup logging
// - Load user session
// Phase 2: Important (async)
// - Initialize database
// - Setup crypto manager
// - Preload first screen data
// Phase 3: Deferred (after first frame)
// - Analytics initialization
// - Push notification setup
// - Background task registration
}
// MARK: - Measurement
static func measureLaunch() {
// Use MetricKit for production
// Use Instruments for development
let launchMetric = MXAppLaunchMetric()
// Track: timeToFirstDraw, resumeTime
}
}
| Operacion |
Max Duration |
Frequency |
Battery Budget |
| Background Fetch |
30s |
15min |
1% max |
| Background Sync |
30s |
On significant change |
0.5% |
| Silent Push Processing |
30s |
As needed |
0.1% |
| Background Task |
3min |
Daily |
2% |
// iOS Background Task Configuration
class BackgroundTaskManager {
// MARK: - Background Fetch
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.medtime.sync",
using: nil
) { task in
self.handleSyncTask(task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.medtime.cleanup",
using: nil
) { task in
self.handleCleanupTask(task as! BGProcessingTask)
}
}
// MARK: - Performance Budgets
struct PerformanceBudget {
static let syncTask = TaskBudget(
maxDuration: .seconds(25), // Leave 5s buffer
maxMemory: 50_000_000, // 50MB
maxCPU: 0.8 // 80% of one core
)
static let cleanupTask = TaskBudget(
maxDuration: .minutes(2),
maxMemory: 100_000_000,
maxCPU: 0.5
)
}
}
3.2.1. View Rendering
| Metrica |
Target |
Warning |
Critical |
| Frame Rate |
60 FPS |
45 FPS |
30 FPS |
| View Update |
<16ms |
16-33ms |
>33ms |
| List Scroll |
0% dropped |
<5% |
>10% |
| Animation |
60 FPS |
50 FPS |
<45 FPS |
// SwiftUI Performance Best Practices
struct PerformantMedicationList: View {
@StateObject private var viewModel = MedicationListViewModel()
var body: some View {
// Use LazyVStack for large lists
ScrollView {
LazyVStack(spacing: 12) {
ForEach(viewModel.medications) { medication in
MedicationRow(medication: medication)
.id(medication.id) // Stable identity
}
}
}
// Avoid recomputing entire list
.equatable()
}
}
// MARK: - Optimization Patterns
struct OptimizationPatterns {
// 1. Extract expensive views
struct ExpensiveView: View, Equatable {
let data: SomeData
var body: some View {
// Complex view
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.data.id == rhs.data.id
}
}
// 2. Use @StateObject for view models
// 3. Prefer value types
// 4. Minimize @State changes
// 5. Use .drawingGroup() for complex graphics
}
// MARK: - Debug Performance
extension View {
func debugPerformance() -> some View {
#if DEBUG
Self._printChanges() // Xcode 13+
return self
#else
return self
#endif
}
}
3.2.2. Image Handling
| Operacion |
Target |
Max |
| Thumbnail Load |
<50ms |
100ms |
| Full Image Load |
<200ms |
500ms |
| Image Decode |
<100ms |
300ms |
| Memory per Image |
5MB |
20MB |
// Efficient Image Loading
struct OptimizedAsyncImage: View {
let url: URL?
let size: CGSize
var body: some View {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
SkeletonView()
.frame(width: size.width, height: size.height)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.clipped()
case .failure:
PlaceholderImage()
@unknown default:
EmptyView()
}
}
}
}
// MARK: - Image Cache Configuration
class ImageCacheManager {
static let shared = ImageCacheManager()
let cache: URLCache = {
let cache = URLCache(
memoryCapacity: 50_000_000, // 50 MB memory
diskCapacity: 100_000_000, // 100 MB disk
directory: .cachesDirectory
)
return cache
}()
// Prefetch visible items
func prefetchImages(urls: [URL]) {
let prefetcher = ImagePrefetcher(urls: urls)
prefetcher.start()
}
}
| Operacion |
Target |
Max |
Items |
| Single Fetch |
5ms |
20ms |
1 |
| List Fetch |
20ms |
50ms |
50 |
| Batch Fetch |
50ms |
150ms |
500 |
| Predicate Query |
30ms |
100ms |
Complex |
| Sort Operation |
10ms |
50ms |
100 items |
// SwiftData Query Optimization
@Model
class Medication {
// MARK: - Indexed Properties
@Attribute(.unique)
var id: UUID
// Index frequently queried fields
@Attribute
var name: String
@Attribute
var nextDoseTime: Date?
@Attribute
var isActive: Bool
// MARK: - Relationships (Lazy by default)
@Relationship(deleteRule: .cascade)
var schedules: [Schedule]?
// MARK: - Computed (not persisted)
var upcomingDoses: [ScheduledDose] {
// Compute on demand, don't persist
}
}
// MARK: - Optimized Queries
class MedicationRepository {
private let context: ModelContext
// Fetch with predicate and sort (single query)
func fetchActiveMedications() async throws -> [Medication] {
let descriptor = FetchDescriptor<Medication>(
predicate: #Predicate { $0.isActive == true },
sortBy: [SortDescriptor(\.nextDoseTime)]
)
return try context.fetch(descriptor)
}
// Paginated fetch for large datasets
func fetchMedications(page: Int, pageSize: Int = 50) async throws -> [Medication] {
var descriptor = FetchDescriptor<Medication>(
sortBy: [SortDescriptor(\.name)]
)
descriptor.fetchLimit = pageSize
descriptor.fetchOffset = page * pageSize
return try context.fetch(descriptor)
}
// Batch insert for sync
func batchInsert(_ medications: [Medication]) async throws {
// Use transaction for batch operations
try context.transaction {
for medication in medications {
context.insert(medication)
}
}
}
}
| Migracion |
Target |
Max |
| Lightweight |
<100ms |
500ms |
| Custom (100 records) |
<1s |
3s |
| Custom (1000 records) |
<5s |
15s |
// Migration Performance Configuration
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
// Lightweight migration (automatic)
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
)
// Custom migration with progress
static func customMigration(
context: ModelContext,
progress: @escaping (Double) -> Void
) async throws {
let batchSize = 100
let total = try context.fetchCount(FetchDescriptor<OldModel>())
var processed = 0
while processed < total {
// Process in batches
var descriptor = FetchDescriptor<OldModel>()
descriptor.fetchLimit = batchSize
descriptor.fetchOffset = processed
let batch = try context.fetch(descriptor)
for item in batch {
// Transform
let newItem = NewModel(from: item)
context.insert(newItem)
}
try context.save()
processed += batch.count
progress(Double(processed) / Double(total))
}
}
}
3.4.1. Encryption Benchmarks
| Operacion |
Target |
Max |
Hardware |
| AES-GCM Encrypt (1KB) |
3ms |
10ms |
SE if available |
| AES-GCM Decrypt (1KB) |
2ms |
8ms |
SE if available |
| Key Generation |
5ms |
20ms |
Secure Enclave |
| Key Derivation (Argon2id) |
200ms |
500ms |
CPU (Swift-Argon2) |
| Keychain Read |
5ms |
30ms |
Secure Enclave |
DV2-REMEDIACION: Argon2id unificado para ambas plataformas.
Decision del Director: 2025-12-09.
CryptoKit no soporta Argon2id nativamente, usar Swift-Argon2 library.
// CryptoKit Performance Implementation
class CryptoManager {
private let symmetricKey: SymmetricKey
// MARK: - Optimized Encryption
func encrypt(_ data: Data) throws -> Data {
// Use Secure Enclave when available
let sealedBox = try AES.GCM.seal(
data,
using: symmetricKey,
nonce: AES.GCM.Nonce()
)
return sealedBox.combined!
}
func decrypt(_ data: Data) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: symmetricKey)
}
// MARK: - Batch Operations
func encryptBatch(_ items: [Data]) async throws -> [Data] {
// Process in parallel for large batches
try await withThrowingTaskGroup(of: (Int, Data).self) { group in
for (index, item) in items.enumerated() {
group.addTask {
(index, try self.encrypt(item))
}
}
var results = [(Int, Data)]()
for try await result in group {
results.append(result)
}
return results.sorted { $0.0 < $1.0 }.map { $0.1 }
}
}
// MARK: - Argon2id Key Derivation (DV2-REMEDIACION)
// Usar Swift-Argon2 library: https://github.com/nicktrienenern/Swift-Argon2
func deriveKey(password: String, salt: Data) throws -> SymmetricKey {
// Argon2id parameters matching 04-seguridad-cliente.md
let hash = try Argon2.hash(
password: password,
salt: salt,
iterations: 3, // t = 3
memory: 64 * 1024, // m = 64MB
parallelism: 4, // p = 4
length: 32, // 256-bit key
variant: .id // Argon2id
)
return SymmetricKey(data: hash)
}
// MARK: - Performance Measurement
func measureEncryptionPerformance() {
let testData = Data(repeating: 0, count: 1024) // 1KB
let start = CFAbsoluteTimeGetCurrent()
for _ in 0..<1000 {
_ = try? encrypt(testData)
}
let elapsed = CFAbsoluteTimeGetCurrent() - start
print("1000 encryptions: \(elapsed)s")
print("Average: \(elapsed / 1000 * 1000)ms")
}
}
3.5.1. URLSession Optimization
| Metrica |
Target |
Max |
| DNS Lookup |
<50ms |
200ms |
| TCP Handshake |
<100ms |
300ms |
| TLS Handshake |
<150ms |
400ms |
| Time to First Byte |
<300ms |
800ms |
| Total Request (1KB) |
<500ms |
1s |
// URLSession Configuration for Performance
class NetworkManager {
static let shared = NetworkManager()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
// Connection pooling
config.httpMaximumConnectionsPerHost = 4
// Timeouts
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
// Caching
config.urlCache = URLCache(
memoryCapacity: 20_000_000, // 20MB
diskCapacity: 100_000_000, // 100MB
directory: nil
)
config.requestCachePolicy = .returnCacheDataElseLoad
// HTTP/2 multiplexing
config.httpShouldUsePipelining = true
// Compression
config.httpAdditionalHeaders = [
"Accept-Encoding": "gzip, deflate"
]
return URLSession(configuration: config)
}()
// MARK: - Request Batching
func batchRequests<T: Decodable>(_ requests: [URLRequest]) async throws -> [T] {
try await withThrowingTaskGroup(of: T.self) { group in
for request in requests {
group.addTask {
let (data, _) = try await self.session.data(for: request)
return try JSONDecoder().decode(T.self, from: data)
}
}
var results = [T]()
for try await result in group {
results.append(result)
}
return results
}
}
// MARK: - Retry with Backoff
func requestWithRetry<T: Decodable>(
_ request: URLRequest,
maxRetries: Int = 3
) async throws -> T {
var lastError: Error?
var delay: UInt64 = 1_000_000_000 // 1 second
for attempt in 0..<maxRetries {
do {
let (data, _) = try await session.data(for: request)
return try JSONDecoder().decode(T.self, from: data)
} catch {
lastError = error
if attempt < maxRetries - 1 {
try await Task.sleep(nanoseconds: delay)
delay *= 2 // Exponential backoff
}
}
}
throw lastError!
}
}
| Metrica |
Target |
Max |
| Timeline Generation |
<100ms |
500ms |
| View Rendering |
<50ms |
200ms |
| Memory Usage |
<30MB |
50MB |
| Refresh Frequency |
15min min |
Configurable |
// WidgetKit Performance Optimization
struct MedicationWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "medication",
provider: MedicationTimelineProvider()
) { entry in
MedicationWidgetView(entry: entry)
}
.configurationDisplayName("Next Medication")
.description("Shows your next scheduled medication")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct MedicationTimelineProvider: TimelineProvider {
// MARK: - Performance Optimizations
func getTimeline(in context: Context, completion: @escaping (Timeline<MedicationEntry>) -> Void) {
// Use background context
Task.detached(priority: .utility) {
let entries = await generateEntries()
// Limit entries to reduce memory
let limitedEntries = Array(entries.prefix(24)) // 24 hours
let timeline = Timeline(
entries: limitedEntries,
policy: .after(Date().addingTimeInterval(900)) // 15 min
)
completion(timeline)
}
}
private func generateEntries() async -> [MedicationEntry] {
// Fetch only what's needed
let medications = await fetchUpcomingMedications(limit: 10)
return medications.map { medication in
MedicationEntry(
date: medication.nextDoseTime,
medication: medication.lightweightCopy // Minimize data
)
}
}
}
// Lightweight data for widget
struct LightweightMedication: Codable {
let id: UUID
let name: String
let dosage: String
let time: Date
// No relationships, no heavy data
}
| Metrica |
Target |
Max |
| Phone-Watch Sync |
<1s |
3s |
| Complication Update |
<100ms |
500ms |
| App Launch |
<1s |
2s |
| Memory Usage |
<20MB |
30MB |
// WatchOS Performance Optimization
class WatchConnectivityManager: NSObject, WCSessionDelegate {
// MARK: - Efficient Data Transfer
func sendMedicationUpdate(_ medication: LightweightMedication) {
guard WCSession.default.isReachable else {
// Queue for later
queueMessage(medication)
return
}
// Use transferUserInfo for guaranteed delivery
let data = try? JSONEncoder().encode(medication)
WCSession.default.transferUserInfo([
"medication": data as Any,
"timestamp": Date()
])
}
// MARK: - Complication Updates
func updateComplication() {
let server = CLKComplicationServer.sharedInstance()
guard let complications = server.activeComplications else { return }
// Batch update
for complication in complications {
server.reloadTimeline(for: complication)
}
}
// MARK: - Data Minimization
struct WatchMedicationData: Codable {
// Only essential fields for watch
let name: String
let dosage: String
let nextDoseTime: Date
let color: String // For quick identification
// Total: ~200 bytes vs 2KB full medication
}
}
4.1. App Lifecycle
PERF-BAJO-002: Android benchmarks no estan al nivel de iOS
Remediacion: Agregar targets especificos para Android con justificacion
| Metrica |
iOS Target |
Android Target |
Variance |
Medicion |
| Cold Start |
<2s |
<2.5s |
+25% |
TTID + TTFD |
| Warm Start |
<500ms |
<600ms |
+20% |
Resume time |
| Hot Start |
<100ms |
<200ms |
+100% |
onResume |
| First Frame |
<1s |
<1.2s |
+20% |
First draw |
Justificacion de Diferencias Android/iOS:
Android_Performance_Justification:
cold_start_overhead:
factor: +25% vs iOS
reasons:
- "Dex/ART compilation overhead (vs iOS pre-compiled)"
- "Fragmented ecosystem: must target older devices"
- "GC initialization + memory management overhead"
- "Broader device spectrum (low-end devices comunes)"
- "SQLCipher overhead mayor en Android (JNI boundary)"
mitigation:
- "Baseline Profiles (R8 optimization)"
- "Startup Profiler integration"
- "Lazy initialization mas agresiva"
warm_start_overhead:
factor: +20% vs iOS
reasons:
- "Process recreation mas frecuente (memoria limitada)"
- "Bundle restore overhead"
- "GC puede triggerearse durante startup"
mitigation:
- "onSaveInstanceState optimization"
- "ViewModel retention"
hot_start_overhead:
factor: +100% vs iOS
reasons:
- "Activity lifecycle mas complejo que SwiftUI"
- "onResume callbacks pueden ser pesados"
- "View inflation si no cached"
mitigation:
- "View binding caching"
- "Deferred onResume work"
acceptance_criteria:
- "Targets Android reflejan realidad del ecosistema"
- "Low-end devices (Samsung A13) deben cumplir max thresholds"
- "High-end devices (Pixel 8) deben superar iOS targets"
- "Optimization continua reduce gap iOS/Android"
Android-Specific Optimization Targets:
| Optimization |
iOS |
Android |
Impact |
| Baseline Profile |
N/A |
Required |
-15% startup |
| R8 Full Mode |
N/A |
Required |
-20% app size |
| ProGuard Rules |
N/A |
Required |
-10% startup |
| Startup Tasks |
Phase 1-3 |
Phase 1-3 + WorkManager |
Parity |
| Memory Pressure |
Rare |
Common |
+defensive GC |
Android_Device_Benchmarks:
low_end_samsung_a13:
cold_start:
target: 3.5s
max: 4.5s
justificacion: "2GB RAM, MediaTek Helio G80"
warm_start:
target: 800ms
max: 1.2s
memory_baseline:
target: 100MB
max: 150MB
mid_range_pixel_6:
cold_start:
target: 2.5s
max: 3.5s
justificacion: "8GB RAM, Google Tensor"
warm_start:
target: 600ms
max: 900ms
memory_baseline:
target: 80MB
max: 120MB
high_end_pixel_8_pro:
cold_start:
target: 1.8s # Beat iOS target
max: 2.5s
justificacion: "12GB RAM, Tensor G3"
warm_start:
target: 400ms # Beat iOS target
max: 600ms
memory_baseline:
target: 80MB
max: 100MB
// Android Launch Optimization
class MedTimeApplication : Application() {
override fun onCreate() {
super.onCreate()
// Phase 1: Critical (blocking, <500ms)
initializeCrashReporting()
initializeLogging()
// Phase 2: Background (non-blocking)
lifecycleScope.launch(Dispatchers.Default) {
initializeDatabase()
initializeCryptoManager()
}
// Phase 3: Deferred (after first frame)
MainScope().launch {
delay(1000) // Wait for UI
initializeAnalytics()
initializePushNotifications()
}
}
}
// Measure with Android Vitals
object LaunchMetrics {
fun reportLaunchTime() {
val launchTime = System.currentTimeMillis() - processStartTime
if (launchTime > 2500) {
// Log slow start
FirebasePerformance.getInstance()
.newTrace("slow_cold_start")
.putMetric("duration_ms", launchTime)
.stop()
}
}
// Use App Startup Library for initialization
class DatabaseInitializer : Initializer<Database> {
override fun create(context: Context): Database {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"medtime.db"
).build()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(CryptoInitializer::class.java)
}
}
}
| Operacion |
Max Duration |
Frequency |
Battery Budget |
| WorkManager Sync |
10min |
15min (flexible) |
1% max |
| Foreground Service |
Unlimited |
As needed |
2%/hour |
| Doze-compatible |
30s window |
Maintenance |
0.5% |
| Expedited Work |
1min |
Critical only |
0.3% |
// WorkManager Configuration for Performance
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
try {
// Set progress for long operations
setProgress(workDataOf("status" to "syncing"))
val syncResult = syncManager.performSync()
if (syncResult.success) {
Result.success()
} else {
Result.retry()
}
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
companion object {
fun schedule(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES,
5, TimeUnit.MINUTES // Flex interval
)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
1, TimeUnit.MINUTES
)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
}
}
4.2.1. Recomposition
| Metrica |
Target |
Warning |
Critical |
| Recomposition Count |
Minimal |
+20% |
+50% |
| Recomposition Time |
<8ms |
8-16ms |
>16ms |
| Skip Rate |
>90% |
70-90% |
<70% |
| Frame Time |
<16ms |
16-32ms |
>32ms |
// Compose Performance Best Practices
@Composable
fun MedicationListScreen(
viewModel: MedicationListViewModel = hiltViewModel()
) {
// Collect as state (not collectAsStateWithLifecycle for lists)
val medications by viewModel.medications.collectAsState()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp)
) {
items(
items = medications,
key = { it.id } // Stable keys prevent recomposition
) { medication ->
MedicationItem(
medication = medication,
onClick = { viewModel.onMedicationClick(it) }
)
}
}
}
// Stable data class
@Immutable
data class MedicationUiModel(
val id: String,
val name: String,
val dosage: String,
val nextDoseTime: String,
val isOverdue: Boolean
)
// Skippable composable
@Composable
private fun MedicationItem(
medication: MedicationUiModel,
onClick: (String) -> Unit
) {
// Use remember for lambdas
val clickHandler = remember(medication.id) {
{ onClick(medication.id) }
}
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = clickHandler)
) {
// Content
}
}
// Debug recomposition
@Composable
fun RecompositionTracker(name: String) {
val recompositions = remember { mutableIntStateOf(0) }
SideEffect {
recompositions.intValue++
Log.d("Recomposition", "$name: ${recompositions.intValue}")
}
}
4.2.2. LazyColumn Optimization
| Metrica |
50 items |
500 items |
5000 items |
| Initial Render |
50ms |
80ms |
100ms |
| Scroll FPS |
60 |
60 |
55-60 |
| Memory |
20MB |
40MB |
60MB |
// LazyColumn Optimization
@Composable
fun OptimizedMedicationList(
medications: List<MedicationUiModel>,
onItemClick: (String) -> Unit
) {
// Use rememberLazyListState for scroll position
val listState = rememberLazyListState()
// Prefetch visible items
val visibleItems = remember(listState) {
derivedStateOf {
listState.layoutInfo.visibleItemsInfo.map { it.index }
}
}
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
items(
items = medications,
key = { it.id },
contentType = { "medication" } // Same type = better recycling
) { medication ->
// Use derivedStateOf for computed properties
val isVisible by remember(visibleItems) {
derivedStateOf { visibleItems.value.contains(medications.indexOf(medication)) }
}
MedicationRow(
medication = medication,
isVisible = isVisible,
onClick = onItemClick
)
}
}
}
// Image loading with Coil
@Composable
fun MedicationImage(
url: String?,
modifier: Modifier = Modifier
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.size(100, 100) // Request specific size
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null,
modifier = modifier,
placeholder = painterResource(R.drawable.placeholder)
)
}
| Operacion |
Target |
Max |
Items |
| Single Fetch |
5ms |
20ms |
1 |
| List Fetch |
25ms |
60ms |
50 |
| Batch Fetch |
60ms |
180ms |
500 |
| Complex Query |
40ms |
120ms |
With joins |
| Transaction |
10ms/item |
30ms/item |
Batch |
// Room Query Optimization
@Dao
interface MedicationDao {
// Indexed query
@Query("SELECT * FROM medications WHERE id = :id")
suspend fun getById(id: String): MedicationEntity?
// Paginated query with Flow
@Query("SELECT * FROM medications WHERE is_active = 1 ORDER BY name LIMIT :limit OFFSET :offset")
fun getActivePaginated(limit: Int, offset: Int): Flow<List<MedicationEntity>>
// Optimized for list display (only needed fields)
@Query("""
SELECT id, name, dosage, next_dose_time
FROM medications
WHERE is_active = 1
ORDER BY next_dose_time
""")
fun getUpcomingDoses(): Flow<List<MedicationSummary>>
// Batch insert with transaction
@Transaction
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(medications: List<MedicationEntity>)
// Batch update
@Query("UPDATE medications SET sync_status = :status WHERE id IN (:ids)")
suspend fun updateSyncStatus(ids: List<String>, status: String)
}
// Projection for list view (lighter)
data class MedicationSummary(
val id: String,
val name: String,
val dosage: String,
@ColumnInfo(name = "next_dose_time")
val nextDoseTime: Long?
)
// Database with SQLCipher
@Database(
entities = [MedicationEntity::class, ScheduleEntity::class],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
companion object {
fun create(context: Context, passphrase: ByteArray): AppDatabase {
val factory = SupportFactory(passphrase)
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"medtime_encrypted.db"
)
.openHelperFactory(factory)
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // Better performance
.build()
}
}
}
| Operacion |
Target |
Max |
Notes |
| AES-GCM Encrypt (1KB) |
5ms |
20ms |
Hardware if available |
| AES-GCM Decrypt (1KB) |
4ms |
15ms |
Hardware if available |
| Argon2id |
200ms |
500ms |
Memory: 64MB |
| Keystore Read |
15ms |
50ms |
StrongBox if available |
// Crypto Performance Implementation
class CryptoManager @Inject constructor(
private val keyManager: KeyManager
) {
private val cipher = Cipher.getInstance("AES/GCM/NoPadding")
// MARK: - Encryption
suspend fun encrypt(data: ByteArray): ByteArray = withContext(Dispatchers.Default) {
val key = keyManager.getMasterKey()
val iv = generateIV()
cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
val encrypted = cipher.doFinal(data)
// Return IV + encrypted
iv + encrypted
}
suspend fun decrypt(data: ByteArray): ByteArray = withContext(Dispatchers.Default) {
val iv = data.sliceArray(0 until 12)
val encrypted = data.sliceArray(12 until data.size)
val key = keyManager.getMasterKey()
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
cipher.doFinal(encrypted)
}
// MARK: - Key Derivation
suspend fun deriveKey(password: String, salt: ByteArray): ByteArray {
return withContext(Dispatchers.Default) {
// Argon2id parameters matching 04-seguridad-cliente.md
val argon2 = Argon2Kt()
val result = argon2.hash(
mode = Argon2Mode.ARGON2_ID,
password = password.toByteArray(),
salt = salt,
tCostInIterations = 3,
mCostInKibibyte = 64 * 1024, // 64 MiB
parallelism = 4,
hashLengthInBytes = 32
)
result.rawHashAsByteArray()
}
}
// MARK: - Performance Measurement
fun measureCryptoPerformance(): CryptoMetrics {
val testData = ByteArray(1024) { it.toByte() }
val iterations = 1000
val encryptStart = System.nanoTime()
repeat(iterations) {
runBlocking { encrypt(testData) }
}
val encryptTime = (System.nanoTime() - encryptStart) / iterations / 1_000_000.0
return CryptoMetrics(
encryptMs = encryptTime,
decryptMs = /* similar measurement */,
keyDerivationMs = /* measure Argon2id */
)
}
}
| Metrica |
Target |
Max |
| Connection Pool |
5 connections |
10 max |
| Keep-Alive |
5 minutes |
- |
| Timeout (connect) |
10s |
- |
| Timeout (read) |
30s |
- |
| Retry Count |
3 |
5 max |
// OkHttp Configuration
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
// Connection pooling
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
// Timeouts
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// Interceptors
.addInterceptor(AuthInterceptor())
.addInterceptor(RetryInterceptor(maxRetries = 3))
.addInterceptor(GzipInterceptor())
// Cache
.cache(Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L // 50 MB
))
// HTTP/2
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()
}
// Retry with exponential backoff
class RetryInterceptor(private val maxRetries: Int) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var lastException: IOException? = null
var delay = 1000L
repeat(maxRetries) { attempt ->
try {
return chain.proceed(chain.request())
} catch (e: IOException) {
lastException = e
if (attempt < maxRetries - 1) {
Thread.sleep(delay)
delay *= 2
}
}
}
throw lastException!!
}
}
}
| Metrica |
Target |
Max |
| Widget Update |
<200ms |
500ms |
| Memory Usage |
<30MB |
50MB |
| Compose Render |
<100ms |
300ms |
// Glance Widget Optimization
class MedicationWidget : GlanceAppWidget() {
override val stateDefinition = GlanceStateDefinition<MedicationWidgetState>()
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
val state = currentState<MedicationWidgetState>()
MedicationWidgetContent(state)
}
}
}
@Composable
private fun MedicationWidgetContent(state: MedicationWidgetState) {
// Use Glance composables (not regular Compose)
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(ColorProvider(Color.White))
.padding(12.dp)
) {
Text(
text = state.medicationName,
style = TextStyle(fontWeight = FontWeight.Bold)
)
Text(
text = "Next: ${state.nextDoseTime}",
style = TextStyle(fontSize = 14.sp)
)
Button(
text = "Take",
onClick = actionRunCallback<TakeDoseAction>()
)
}
}
// Efficient state updates
class MedicationWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget = MedicationWidget()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
// Batch updates
if (intent.action == "UPDATE_WIDGET") {
MainScope().launch {
val state = fetchWidgetState()
updateAppWidgetState(context, glanceId, state)
glanceAppWidget.update(context, glanceId)
}
}
}
}
| Metrica |
Target |
Max |
| Data Sync |
<500ms |
2s |
| Message Delivery |
<200ms |
1s |
| Complication Update |
<100ms |
500ms |
| Memory |
<25MB |
40MB |
// Wear OS Performance
class WearDataManager(
private val dataClient: DataClient,
private val messageClient: MessageClient
) {
// Efficient data sync
suspend fun syncMedications(medications: List<LightweightMedication>) {
val dataMap = PutDataMapRequest.create("/medications").run {
dataMap.putDataMapArrayList(
"medications",
medications.map { it.toDataMap() }.toArrayList()
)
dataMap.putLong("timestamp", System.currentTimeMillis())
asPutDataRequest().setUrgent()
}
dataClient.putDataItem(dataMap).await()
}
// Lightweight medication for watch
data class LightweightMedication(
val id: String,
val name: String,
val dosage: String,
val nextDoseTime: Long,
val color: Int
) {
fun toDataMap(): DataMap = DataMap().apply {
putString("id", id)
putString("name", name)
putString("dosage", dosage)
putLong("nextDoseTime", nextDoseTime)
putInt("color", color)
}
}
// Complication update
fun updateComplication(context: Context) {
val request = ComplicationDataSourceUpdateRequester.create(
context,
ComponentName(context, MedicationComplicationService::class.java)
)
request.requestUpdateAll()
}
}
5.1. Feature Parity Matrix
| Feature |
iOS Target |
Android Target |
Variance Allowed |
| Cold Start |
2s |
2.5s |
+25% Android |
| List Scroll |
60 FPS |
60 FPS |
0% |
| Encryption (1KB) |
3ms |
5ms |
+67% Android |
| Sync Push |
500ms |
500ms |
0% |
| Memory (baseline) |
50MB |
80MB |
+60% Android |
| Battery (foreground) |
5%/hr |
5%/hr |
0% |
5.2. Comparative Benchmarks
Cross-Platform Benchmarks:
app_launch:
measurement: "Cold start to interactive"
ios:
target: 2000ms
low_end_device: iPhone SE 2nd
high_end_device: iPhone 15 Pro
android:
target: 2500ms
low_end_device: Samsung A13
high_end_device: Pixel 8 Pro
list_performance:
measurement: "Scroll FPS with 500 items"
ios:
target: 60fps
technology: SwiftUI LazyVStack
android:
target: 60fps
technology: Compose LazyColumn
encryption_throughput:
measurement: "AES-GCM operations per second"
ios:
target: 333/s # 3ms per operation
technology: CryptoKit + Secure Enclave
android:
target: 200/s # 5ms per operation
technology: Cipher + StrongBox
sync_performance:
measurement: "100 items incremental sync"
ios:
target: 2s
technology: URLSession + async/await
android:
target: 2s
technology: Retrofit + Coroutines
6. Testing Methodology
6.1. Device Matrix
| Category |
iOS Devices |
Android Devices |
| Low-end |
iPhone SE (2nd), iPhone 8 |
Samsung A13, Moto G Power |
| Mid-range |
iPhone 12, iPhone 13 |
Pixel 6, Samsung A54 |
| High-end |
iPhone 15 Pro, iPhone 15 Pro Max |
Pixel 8 Pro, Samsung S24 Ultra |
PERF-BAJO-001: Screen transitions > 300ms para algunas pantallas
Remediacion: Documentar excepciones aceptables o targets especificos
6.2.1. Targets por Tipo de Transicion
| Tipo de Transicion |
Target |
Max |
Excepciones Aceptables |
| Simple (navegacion) |
<150ms |
300ms |
N/A |
| Lista → Detalle |
<200ms |
300ms |
N/A |
| Modal overlay |
<100ms |
200ms |
N/A |
| Tab switch |
<100ms |
150ms |
N/A |
| Deep link navigation |
<300ms |
500ms |
Incluye data loading |
| Initial app load → Home |
<500ms |
1s |
Incluye crypto setup |
6.2.2. Excepciones Documentadas (>300ms)
Excepciones_Aceptables:
initial_home_load:
target: 500ms
max: 1000ms
justificacion: >
Primera carga incluye:
- Key retrieval (Keychain/Keystore)
- Database connection
- Decryption inicial de datos
- UI rendering
mitigation:
- Mostrar skeleton/loading state
- Progressive reveal de UI
- Cache en memoria para siguientes accesos
deep_link_with_data:
target: 300ms
max: 500ms
justificacion: >
Deep link requiere:
- Route parsing
- Data fetch desde DB local
- Potential decryption
mitigation:
- Optimistic UI
- Placeholder mientras carga
search_results_load:
target: 300ms
max: 500ms
justificacion: >
Busqueda con:
- Query de DB compleja
- Multiple decryptions
- Rendering de lista grande
mitigation:
- Debounce (300ms)
- Paginated results
- Virtual scrolling
offline_to_online_sync:
target: 500ms
max: 1000ms
justificacion: >
Transicion durante sync incluye:
- Network reconnection
- Pending operations flush
- UI state reconciliation
mitigation:
- Non-blocking sync
- Progress indicator
- Allow navigation durante sync
6.2.3. Medicion de Transitions
// iOS: Measure transition time
class NavigationPerformanceMonitor {
static func measureTransition(from: String, to: String) {
let start = CFAbsoluteTimeGetCurrent()
// On destination view appear
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
if elapsed > 300 {
print("SLOW TRANSITION: \(from) → \(to): \(elapsed)ms")
}
}
}
// Android: Measure transition time
class NavigationPerformanceMonitor {
companion object {
fun measureTransition(from: String, to: String) {
val startTime = SystemClock.elapsedRealtime()
// On destination composable first frame
val elapsed = SystemClock.elapsedRealtime() - startTime
if (elapsed > 300) {
Log.w("PERF", "SLOW TRANSITION: $from → $to: ${elapsed}ms")
}
}
}
}
6.3. Network Conditions
| Profile |
Latency |
Bandwidth |
Loss |
| 5G |
20ms |
100 Mbps |
0% |
| 4G Good |
50ms |
20 Mbps |
0.1% |
| 4G Average |
100ms |
5 Mbps |
0.5% |
| 3G |
300ms |
1 Mbps |
2% |
| Edge |
500ms |
200 Kbps |
5% |
| Offline |
N/A |
0 |
100% |
6.4. Test Scenarios
Performance Test Scenarios:
cold_start:
steps:
1. Force kill app
2. Clear memory pressure
3. Launch app
4. Measure to first interactive frame
repeat: 10
metrics:
- time_to_first_frame
- time_to_interactive
- memory_at_launch
list_scroll:
steps:
1. Load list with 500 items
2. Scroll to bottom at constant velocity
3. Scroll back to top
repeat: 5
metrics:
- average_fps
- dropped_frames
- jank_percentage
sync_stress:
steps:
1. Queue 100 pending operations
2. Trigger sync
3. Measure completion
conditions:
- 4G network
- battery > 50%
metrics:
- total_time
- operations_per_second
- retry_count
encryption_benchmark:
steps:
1. Generate 1000 random 1KB blobs
2. Encrypt all blobs
3. Decrypt all blobs
repeat: 3
metrics:
- encrypt_time_avg
- decrypt_time_avg
- memory_peak
background_battery:
steps:
1. Enable all background tasks
2. Run for 4 hours (simulated)
3. Measure battery drain
conditions:
- screen off
- WiFi connected
metrics:
- battery_drain_percent
- wake_lock_duration
- network_operations
7. Optimization Strategies
7.1. Quick Wins
| Optimization |
Impact |
Effort |
Priority |
| Lazy initialization |
High |
Low |
P0 |
| Image caching |
High |
Low |
P0 |
| List virtualization |
High |
Medium |
P0 |
| Query optimization |
Medium |
Medium |
P1 |
| Memory cleanup |
Medium |
Low |
P1 |
| Background batching |
Medium |
Medium |
P2 |
7.2. Advanced Optimizations
Advanced Optimizations:
startup:
- Baseline Profile (Android)
- App thinning (iOS)
- Deferred initialization
- Precomputed layouts
rendering:
- Compose stability annotations (Android)
- View recycling optimization
- Offscreen composition
- Hardware acceleration
memory:
- Image downsampling
- LRU cache tuning
- Weak references for caches
- Aggressive GC hints
network:
- HTTP/2 multiplexing
- Request batching
- Predictive prefetching
- Compression optimization
database:
- WAL mode
- Index optimization
- Query plan analysis
- Connection pooling
7.3. Monitoring in Production
Production Monitoring:
ios:
tools:
- MetricKit (Apple)
- Firebase Performance
- Sentry
metrics:
- launch_time
- hang_rate
- memory_peak
- battery_drain
android:
tools:
- Android Vitals
- Firebase Performance
- Sentry
metrics:
- startup_time
- slow_rendering
- anr_rate
- excessive_wakeups
alerts:
p95_launch_time:
ios: "> 3s"
android: "> 4s"
crash_free_rate:
both: "< 99.5%"
anr_rate:
android: "> 0.5%"
8. Apendice
8.1. Profiling Commands
# iOS
# Launch with Instruments
xcrun xctrace record --template 'Time Profiler' --launch com.medtime.app
# Android
# Launch with profiler
adb shell am start -W -n com.medtime.app/.MainActivity
# Capture systrace
python systrace.py -o trace.html sched freq idle am wm gfx view
# Memory dump
adb shell dumpsys meminfo com.medtime.app
8.2. Benchmark Results Template
| Metric |
Target |
Actual |
Status |
Device |
| Cold Start |
2s |
1.8s |
PASS |
iPhone 12 |
| Cold Start |
2.5s |
2.3s |
PASS |
Pixel 6 |
| List Scroll |
60 FPS |
58 FPS |
WARN |
iPhone SE |
| Encryption |
5ms |
4ms |
PASS |
Both |
Documento generado por SpecQueen Technical Division - IT-14
"Smooth is fast. Fast is smooth."