android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose-optimization
|
|
3
|
+
description: >
|
|
4
|
+
Jetpack Compose performance optimization.
|
|
5
|
+
Load this skill when reducing unnecessary recompositions, using
|
|
6
|
+
stability annotations, optimizing LazyList performance, profiling
|
|
7
|
+
with Layout Inspector, or fixing skipped frames in Compose UI.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Compose Optimization
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Compose performance is dominated by **recomposition** — the process of re-running composables when state changes. Unnecessary recompositions cause dropped frames. The goal is to ensure only the composables that actually depend on changed state are recomposed, using stable types, correct key usage, and proper state scoping.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Recomposition is triggered by **state reads** — scope reads as narrowly as possible
|
|
20
|
+
- Use **stable types** for parameters — unstable types cause recomposition on every parent recompose
|
|
21
|
+
- Pass **lambdas** instead of state to leaf composables — reduces recomposition scope
|
|
22
|
+
- Use `key()` in `LazyColumn` — helps Compose identify and reuse items correctly
|
|
23
|
+
- Profile with **Compose Compiler Metrics** and **Layout Inspector** before optimizing
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Stability and @Stable / @Immutable
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ❌ Unstable class — List is not stable (Compose can't verify it won't change)
|
|
31
|
+
data class UserListState(
|
|
32
|
+
val users: List<User>, // mutable List — Compose treats as unstable
|
|
33
|
+
val isLoading: Boolean
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// ✅ Use ImmutableList or @Immutable annotation
|
|
37
|
+
@Immutable
|
|
38
|
+
data class UserListState(
|
|
39
|
+
val users: ImmutableList<User>, // kotlinx.collections.immutable
|
|
40
|
+
val isLoading: Boolean
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// ✅ Or annotate domain model as stable
|
|
44
|
+
@Stable
|
|
45
|
+
data class User(
|
|
46
|
+
val id: String,
|
|
47
|
+
val name: String,
|
|
48
|
+
val email: String
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## State Scoping — Read State as Low as Possible
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
// ❌ Reading state high in the tree — recomposes entire subtree
|
|
58
|
+
@Composable
|
|
59
|
+
fun UserScreen(viewModel: UserViewModel) {
|
|
60
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
61
|
+
// entire screen recomposes when any state field changes
|
|
62
|
+
UserList(users = state.users, isLoading = state.isLoading)
|
|
63
|
+
SearchBar(query = state.searchQuery)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ✅ Pass lambdas to defer state reads to leaf composables
|
|
67
|
+
@Composable
|
|
68
|
+
fun UserScreen(viewModel: UserViewModel) {
|
|
69
|
+
UserList(
|
|
70
|
+
users = { viewModel.state.value.users }, // read inside UserList
|
|
71
|
+
isLoading = { viewModel.state.value.isLoading }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## LazyColumn Optimization
|
|
79
|
+
|
|
80
|
+
```kotlin
|
|
81
|
+
// ✅ Always provide stable keys
|
|
82
|
+
LazyColumn {
|
|
83
|
+
items(
|
|
84
|
+
items = users,
|
|
85
|
+
key = { user -> user.id } // ✅ stable key — enables item reuse
|
|
86
|
+
) { user ->
|
|
87
|
+
UserItem(user = user)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ✅ Use contentType for heterogeneous lists
|
|
92
|
+
LazyColumn {
|
|
93
|
+
items(
|
|
94
|
+
items = feedItems,
|
|
95
|
+
key = { it.id },
|
|
96
|
+
contentType = { item -> item::class } // ✅ different composables for different types
|
|
97
|
+
) { item ->
|
|
98
|
+
when (item) {
|
|
99
|
+
is FeedItem.Post -> PostItem(item)
|
|
100
|
+
is FeedItem.Ad -> AdItem(item)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ✅ rememberLazyListState to preserve scroll position
|
|
106
|
+
val listState = rememberLazyListState()
|
|
107
|
+
LazyColumn(state = listState) { ... }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## remember and derivedStateOf
|
|
113
|
+
|
|
114
|
+
```kotlin
|
|
115
|
+
// ✅ derivedStateOf — compute derived value only when inputs change
|
|
116
|
+
@Composable
|
|
117
|
+
fun UserList(users: List<User>, searchQuery: String) {
|
|
118
|
+
val filteredUsers by remember(users, searchQuery) {
|
|
119
|
+
derivedStateOf {
|
|
120
|
+
users.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// filteredUsers recomputes only when users or searchQuery change
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ✅ remember expensive computation
|
|
127
|
+
@Composable
|
|
128
|
+
fun ExpensiveScreen(data: List<Data>) {
|
|
129
|
+
val processed = remember(data) {
|
|
130
|
+
data.map { processExpensive(it) } // only recomputes when data changes
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Avoiding Lambda Allocations
|
|
138
|
+
|
|
139
|
+
```kotlin
|
|
140
|
+
// ❌ New lambda on every recomposition
|
|
141
|
+
@Composable
|
|
142
|
+
fun UserItem(user: User, onDelete: (User) -> Unit) {
|
|
143
|
+
Button(onClick = { onDelete(user) }) { ... } // new lambda every recompose
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ✅ remember the lambda
|
|
147
|
+
@Composable
|
|
148
|
+
fun UserItem(user: User, onDelete: (User) -> Unit) {
|
|
149
|
+
val onClick = remember(user.id) { { onDelete(user) } }
|
|
150
|
+
Button(onClick = onClick) { ... }
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Compose Compiler Metrics
|
|
157
|
+
|
|
158
|
+
```kotlin
|
|
159
|
+
// build.gradle.kts — enable compiler metrics
|
|
160
|
+
tasks.withType<KotlinCompile>().configureEach {
|
|
161
|
+
kotlinOptions {
|
|
162
|
+
freeCompilerArgs += listOf(
|
|
163
|
+
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir}/compose_metrics",
|
|
164
|
+
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.buildDir}/compose_metrics"
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Then check build/compose_metrics/ for stability reports
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Anti-Patterns
|
|
174
|
+
|
|
175
|
+
- Passing unstable `List<T>` to composables — triggers recomposition unnecessarily
|
|
176
|
+
- Reading `StateFlow.value` directly in composable body — doesn't trigger recomposition
|
|
177
|
+
- No `key` in `LazyColumn` items — items are recreated instead of reused on scroll
|
|
178
|
+
- Doing heavy computation directly in composable body — use `remember` or `derivedStateOf`
|
|
179
|
+
- Deeply nested composables all reading the same state — scope reads lower in the tree
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Related Skills
|
|
184
|
+
- `compose` — Compose fundamentals
|
|
185
|
+
- `compose-performance` — profiling and measuring Compose performance
|
|
186
|
+
- `state-management` — state hoisting and StateFlow patterns
|
|
187
|
+
- `allocation-optimization` — reducing object creation in Compose
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: heap-management
|
|
3
|
+
description: >
|
|
4
|
+
Android heap memory management and optimization.
|
|
5
|
+
Load this skill when investigating OutOfMemoryError, reducing memory
|
|
6
|
+
footprint, understanding heap limits, managing large objects,
|
|
7
|
+
or profiling memory usage with Android Studio Memory Profiler.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Heap Management
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Each Android app has a limited heap size (typically 128–512MB depending on device). Exceeding this limit causes `OutOfMemoryError`. Proper heap management involves avoiding large allocations, reusing objects, releasing memory under pressure, and profiling with Android Studio Memory Profiler.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Never hold large objects (Bitmap, byte arrays) longer than necessary
|
|
20
|
+
- Release memory in `onTrimMemory` — the system signals when memory is low
|
|
21
|
+
- Use object pools for frequently created/destroyed objects
|
|
22
|
+
- Profile memory with **Android Studio Memory Profiler** before optimizing
|
|
23
|
+
- Prefer streaming over loading entire files into memory
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Heap Limits
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Check available heap at runtime
|
|
31
|
+
val runtime = Runtime.getRuntime()
|
|
32
|
+
val maxHeap = runtime.maxMemory() // max heap the app can use
|
|
33
|
+
val usedHeap = runtime.totalMemory() - runtime.freeMemory()
|
|
34
|
+
val availableHeap = maxHeap - usedHeap
|
|
35
|
+
|
|
36
|
+
Timber.d("Heap: used=${usedHeap/1024/1024}MB, max=${maxHeap/1024/1024}MB")
|
|
37
|
+
|
|
38
|
+
// ✅ Request large heap in manifest (use sparingly)
|
|
39
|
+
// android:largeHeap="true" in <application> — last resort
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## onTrimMemory — Release Under Pressure
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ✅ Release non-essential memory when system requests it
|
|
48
|
+
class App : Application() {
|
|
49
|
+
override fun onTrimMemory(level: Int) {
|
|
50
|
+
super.onTrimMemory(level)
|
|
51
|
+
when (level) {
|
|
52
|
+
TRIM_MEMORY_UI_HIDDEN -> {
|
|
53
|
+
// App went to background — release UI caches
|
|
54
|
+
imageCache.evictAll()
|
|
55
|
+
}
|
|
56
|
+
TRIM_MEMORY_RUNNING_CRITICAL,
|
|
57
|
+
TRIM_MEMORY_COMPLETE -> {
|
|
58
|
+
// Critical — release everything possible
|
|
59
|
+
imageCache.evictAll()
|
|
60
|
+
dataCache.clear()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ✅ Implement in ViewModel too
|
|
67
|
+
class UserListViewModel : ViewModel(), ComponentCallbacks2 {
|
|
68
|
+
override fun onTrimMemory(level: Int) {
|
|
69
|
+
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
|
|
70
|
+
cachedUsers.clear()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
override fun onConfigurationChanged(newConfig: Configuration) {}
|
|
74
|
+
override fun onLowMemory() { cachedUsers.clear() }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## LruCache for Memory Caching
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// ✅ LruCache — evicts least-recently-used entries when full
|
|
84
|
+
class ImageMemoryCache {
|
|
85
|
+
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
|
|
86
|
+
private val cacheSize = maxMemory / 8 // use 1/8 of available heap
|
|
87
|
+
|
|
88
|
+
private val cache = object : LruCache<String, Bitmap>(cacheSize) {
|
|
89
|
+
override fun sizeOf(key: String, bitmap: Bitmap): Int {
|
|
90
|
+
return bitmap.byteCount / 1024 // size in KB
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun put(key: String, bitmap: Bitmap) = cache.put(key, bitmap)
|
|
95
|
+
fun get(key: String): Bitmap? = cache.get(key)
|
|
96
|
+
fun evictAll() = cache.evictAll()
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Avoiding Large Allocations
|
|
103
|
+
|
|
104
|
+
```kotlin
|
|
105
|
+
// ❌ Loading entire file into memory
|
|
106
|
+
val bytes = File(path).readBytes() // OOM for large files
|
|
107
|
+
|
|
108
|
+
// ✅ Stream the file
|
|
109
|
+
File(path).inputStream().use { stream ->
|
|
110
|
+
val buffer = ByteArray(8192) // 8KB buffer
|
|
111
|
+
var bytesRead: Int
|
|
112
|
+
while (stream.read(buffer).also { bytesRead = it } != -1) {
|
|
113
|
+
process(buffer, bytesRead)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ❌ Parsing entire JSON response before using it
|
|
118
|
+
val json = response.body?.string() // loads everything into memory
|
|
119
|
+
|
|
120
|
+
// ✅ Stream JSON parsing with Kotlinx Serialization
|
|
121
|
+
val users = json.decodeFromStream<List<UserDto>>(response.body!!.byteStream())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Object Pooling
|
|
127
|
+
|
|
128
|
+
```kotlin
|
|
129
|
+
// ✅ Reuse objects instead of creating new ones in tight loops
|
|
130
|
+
class RectPool {
|
|
131
|
+
private val pool = ArrayDeque<RectF>()
|
|
132
|
+
|
|
133
|
+
fun acquire(): RectF = pool.removeFirstOrNull() ?: RectF()
|
|
134
|
+
|
|
135
|
+
fun release(rect: RectF) {
|
|
136
|
+
rect.setEmpty()
|
|
137
|
+
pool.addLast(rect)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ✅ Usage
|
|
142
|
+
val rectPool = RectPool()
|
|
143
|
+
val rect = rectPool.acquire()
|
|
144
|
+
try {
|
|
145
|
+
// use rect
|
|
146
|
+
} finally {
|
|
147
|
+
rectPool.release(rect)
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Profiling Tools
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
Android Studio Memory Profiler:
|
|
157
|
+
- Heap dump: snapshot of all objects in memory
|
|
158
|
+
- Allocation tracking: which code allocates the most
|
|
159
|
+
- GC events: frequency indicates memory pressure
|
|
160
|
+
|
|
161
|
+
Key metrics to watch:
|
|
162
|
+
- Java Heap: should not grow unboundedly
|
|
163
|
+
- Native Heap: images, bitmaps (decoded natively)
|
|
164
|
+
- Shallow size: size of object itself
|
|
165
|
+
- Retained size: size of object + everything it keeps alive
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Anti-Patterns
|
|
171
|
+
|
|
172
|
+
- Holding Bitmap references in static fields — never GC'd
|
|
173
|
+
- Caching without size limits — unbounded cache grows until OOM
|
|
174
|
+
- Loading full-resolution images for thumbnails — decode at required size
|
|
175
|
+
- Creating many small objects in tight loops — GC pressure
|
|
176
|
+
- Not implementing `onTrimMemory` — app doesn't release memory under pressure
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Related Skills
|
|
181
|
+
- `bitmap-optimization` — efficient Bitmap loading and scaling
|
|
182
|
+
- `memory-leak-prevention` — preventing reference leaks
|
|
183
|
+
- `compose-optimization` — reducing recompositions and allocations
|
|
184
|
+
- `startup-optimization` — reducing memory allocations at startup
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: macrobenchmark
|
|
3
|
+
description: >
|
|
4
|
+
Macrobenchmark for end-to-end performance measurement in Android.
|
|
5
|
+
Load this skill when measuring app startup time, scroll jank,
|
|
6
|
+
navigation transitions, or any user journey performance metric
|
|
7
|
+
using the Macrobenchmark library.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Macrobenchmark
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Macrobenchmark measures complete user journeys — startup, scroll performance, navigation — from outside the app using UI automation. Unlike microbenchmark, it measures the full system including rendering, I/O, and process startup. It is the standard tool for startup time regression testing and scroll jank measurement.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Macrobenchmark runs in a **separate test process** — it instruments the app from outside
|
|
20
|
+
- Always test in **release** or **benchmark** build type — not debug
|
|
21
|
+
- Use `StartupMode.COLD` for startup measurement — ensures process is killed first
|
|
22
|
+
- Test on **physical devices** — emulator performance is not representative
|
|
23
|
+
- Run multiple iterations — Macrobenchmark reports min, median, and max automatically
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# libs.versions.toml
|
|
31
|
+
[libraries]
|
|
32
|
+
androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version = "1.3.0" }
|
|
33
|
+
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.3.0" }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```kotlin
|
|
37
|
+
// macrobenchmark/build.gradle.kts
|
|
38
|
+
plugins {
|
|
39
|
+
alias(libs.plugins.android.test)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
android {
|
|
43
|
+
targetProjectPath = ":app"
|
|
44
|
+
experimentalProperties["android.experimental.self-instrumenting"] = true
|
|
45
|
+
|
|
46
|
+
defaultConfig {
|
|
47
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
48
|
+
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
buildTypes {
|
|
52
|
+
create("benchmark") {
|
|
53
|
+
isDebuggable = true
|
|
54
|
+
signingConfig = signingConfigs.getByName("debug")
|
|
55
|
+
matchingFallbacks += listOf("release")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
dependencies {
|
|
61
|
+
implementation(libs.androidx.benchmark.macro)
|
|
62
|
+
implementation(libs.androidx.test.uiautomator)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Startup Benchmark
|
|
69
|
+
|
|
70
|
+
```kotlin
|
|
71
|
+
@RunWith(AndroidJUnit4::class)
|
|
72
|
+
class StartupBenchmark {
|
|
73
|
+
|
|
74
|
+
@get:Rule
|
|
75
|
+
val benchmarkRule = MacrobenchmarkRule()
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun coldStart() = benchmarkRule.measureRepeated(
|
|
79
|
+
packageName = "com.example.app",
|
|
80
|
+
metrics = listOf(StartupTimingMetric()),
|
|
81
|
+
compilationMode = CompilationMode.Partial(
|
|
82
|
+
baselineProfileMode = BaselineProfileMode.Require
|
|
83
|
+
),
|
|
84
|
+
startupMode = StartupMode.COLD, // kill process before each iteration
|
|
85
|
+
iterations = 5
|
|
86
|
+
) {
|
|
87
|
+
pressHome()
|
|
88
|
+
startActivityAndWait() // wait for first frame
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
fun warmStart() = benchmarkRule.measureRepeated(
|
|
93
|
+
packageName = "com.example.app",
|
|
94
|
+
metrics = listOf(StartupTimingMetric()),
|
|
95
|
+
compilationMode = CompilationMode.Partial(),
|
|
96
|
+
startupMode = StartupMode.WARM, // process alive, Activity recreated
|
|
97
|
+
iterations = 5
|
|
98
|
+
) {
|
|
99
|
+
startActivityAndWait()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Scroll Jank Benchmark
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
@RunWith(AndroidJUnit4::class)
|
|
110
|
+
class ScrollBenchmark {
|
|
111
|
+
|
|
112
|
+
@get:Rule
|
|
113
|
+
val benchmarkRule = MacrobenchmarkRule()
|
|
114
|
+
|
|
115
|
+
@Test
|
|
116
|
+
fun scrollUserList() = benchmarkRule.measureRepeated(
|
|
117
|
+
packageName = "com.example.app",
|
|
118
|
+
metrics = listOf(FrameTimingMetric()), // measures frame timing and jank
|
|
119
|
+
compilationMode = CompilationMode.Partial(),
|
|
120
|
+
startupMode = StartupMode.WARM,
|
|
121
|
+
iterations = 5
|
|
122
|
+
) {
|
|
123
|
+
startActivityAndWait()
|
|
124
|
+
|
|
125
|
+
// Navigate to list screen
|
|
126
|
+
device.findObject(By.text("Users")).click()
|
|
127
|
+
device.waitForIdle()
|
|
128
|
+
|
|
129
|
+
// ✅ Scroll the list — FrameTimingMetric captures frame data
|
|
130
|
+
val list = device.findObject(By.scrollable(true))
|
|
131
|
+
repeat(3) {
|
|
132
|
+
list.fling(Direction.DOWN)
|
|
133
|
+
device.waitForIdle()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Custom Metrics
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
// ✅ Measure time to specific state (e.g. data loaded)
|
|
145
|
+
@Test
|
|
146
|
+
fun timeToDataLoaded() = benchmarkRule.measureRepeated(
|
|
147
|
+
packageName = "com.example.app",
|
|
148
|
+
metrics = listOf(
|
|
149
|
+
StartupTimingMetric(),
|
|
150
|
+
TraceSectionMetric("data_loaded") // custom trace section
|
|
151
|
+
),
|
|
152
|
+
startupMode = StartupMode.COLD,
|
|
153
|
+
iterations = 5
|
|
154
|
+
) {
|
|
155
|
+
pressHome()
|
|
156
|
+
startActivityAndWait()
|
|
157
|
+
// Wait for custom trace section to appear
|
|
158
|
+
device.wait(Until.hasObject(By.text("Users loaded")), 5_000)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// In app code — mark the trace section
|
|
162
|
+
class UserListViewModel : ViewModel() {
|
|
163
|
+
fun loadUsers() {
|
|
164
|
+
viewModelScope.launch {
|
|
165
|
+
val users = repository.getUsers()
|
|
166
|
+
trace("data_loaded") { // ✅ marks trace section for benchmark
|
|
167
|
+
_state.value = UiState.Success(users)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Compilation Modes
|
|
177
|
+
|
|
178
|
+
```kotlin
|
|
179
|
+
// CompilationMode.None — no AOT, pure JIT (worst case baseline)
|
|
180
|
+
// CompilationMode.Partial() — use existing baseline profile if present
|
|
181
|
+
// CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require) — must have profile
|
|
182
|
+
// CompilationMode.Full — fully AOT compiled (best case)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Running and Reading Results
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Run benchmarks
|
|
191
|
+
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
|
192
|
+
|
|
193
|
+
# Results saved to:
|
|
194
|
+
# build/outputs/connected_android_test_additional_output/benchmark/
|
|
195
|
+
# Open in Android Studio via Profiler → Import benchmark results
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Anti-Patterns
|
|
201
|
+
|
|
202
|
+
- Running on emulator — performance not representative
|
|
203
|
+
- Testing in debug build — JIT + debug overhead inflates results
|
|
204
|
+
- Not using `startActivityAndWait()` — benchmark ends before first frame
|
|
205
|
+
- Single iteration — statistical noise makes results unreliable
|
|
206
|
+
- Not using `CompilationMode.None` as baseline — can't see profile improvement
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Related Skills
|
|
211
|
+
- `baseline-profile` — generating profiles measured by Macrobenchmark
|
|
212
|
+
- `benchmark` — microbenchmark for method-level measurement
|
|
213
|
+
- `startup-optimization` — techniques to improve startup metrics
|
|
214
|
+
- `rendering-performance` — understanding frame timing results
|