El estado general de performance de PixelPlayer es mixto con tendencia a acumulativo. La app demuestra que hubo esfuerzo deliberado en optimización: strong skipping, stability config, state slicing con .map().distinctUntilChanged() en el player sheet, separación del flow de posición, baseline profiles, y uso de ImmutableList extensivo. Esto no es un codebase negligente en performance.
Sin embargo, el problema principal es de amplificación acumulativa: individualmente, muchos patrones son "aceptables", pero su combinación simultánea —especialmente en dispositivos de gama media/baja con bibliotecas grandes— genera un budget de frames que se desborda consistentemente. El cuello de botella no es un solo componente catastrófico, sino la suma de:
- Recomposición innecesaria por observación no-sliceada de
PlayerUiStateen 3 tabs de la librería +SearchScreen - Operaciones
O(n)sobre colecciones grandes durante composición o en el main thread pipeline - Animaciones por-item excesivas (hasta 7
animateXAsStateporEnhancedSongListItem) AnimatedVisibilityen cada item individual de secciones expandibles (ArtistDetail,GenreDetail)- Presión de GC por conversiones frecuentes
.toImmutableList()sobre colas grandes
En hardware potente (Snapdragon 8 Gen 2+, 8GB+ RAM), la mayoría de esto queda enmascarado por CPU headroom. En un Snapdragon 680, MediaTek Helio G85, o similar con 4GB RAM y una biblioteca de 5000+ canciones, estas acumulaciones pueden producir jank perceptible en scroll, transiciones de pestaña y apertura de sheets.
Severidad: CRÍTICA
Impacto real: Cada vez que cualquiera de los ~30 campos de PlayerUiState cambia —incluyendo currentPosition que se actualiza cada 250ms, searchResults, lavaLampColors, estado de queue undo, etc.— los tabs de Albums, Artists y Search recomponen completamente, incluyendo sus LazyColumn/LazyGrid, fast scroll labels, sort trackers y LaunchedEffects.
Por qué ocurre:
LibraryMediaTabs.kt:85—LibraryAlbumsTabhaceplayerViewModel.playerUiState.collectAsStateWithLifecycle()para leer soloisAlbumsListViewycurrentAlbumSortOptionLibraryMediaTabs.kt:400—LibraryArtistsTabhace lo mismo para leer solocurrentArtistSortOptionSearchScreen.kt:138— mismo patrón para search state
Contraste: UnifiedPlayerSheetV2.kt ya usa el patrón correcto:
playerViewModel.playerUiState
.map { PlayerUiSheetSliceV2(...) }
.distinctUntilChanged()Pero los tabs de librería no aplican este patrón.
Síntomas: Micro-stutters durante scroll en tabs de Albums/Artists mientras hay música reproduciéndose (el position ticker cada 250ms fuerza recomposición del tab activo aunque el usuario solo está scrolleando). Peor con queue changes, search, o undo bar toggles.
Dispositivos más afectados: Todos, pero es catastrófico en gama media/baja donde un frame budget de 16ms no tiene margen.
Evidencia:
PlayerUiState.kt:19—currentPosition: Longestá dentro del state. Cada tick del position updater enPlaybackStateHolder.kt:553-554actualiza_currentPosition, y si ese valor se propaga aPlayerUiState, todas las tabs se invalidan.- Confirmado:
playerUiStatees unMutableStateFlowseparado con updates imperativos (_playerUiState.update { ... }), y al menos la posición SÍ se actualiza ahí (campocurrentPositionen línea 19).
Severidad: CRÍTICA
Impacto real: En ArtistDetailScreen.kt:358-377, cada canción individual dentro de cada sección de álbum está envuelta en AnimatedVisibility. Al expandir una sección de un artista con 50 canciones, se ejecutan 50 expandVertically + 50 fadeIn simultáneamente. Cada una con su propio Animatable, su propio coroutine de animación y su propio re-layout frame-by-frame.
Por qué ocurre: El patrón AnimatedVisibility aplicado a items individuales en un itemsIndexed de LazyColumn fuerza:
- Materialización de todos los items de la sección al momento del toggle (
LazyColumnlos necesita para medir la animación) 50+ expandVertically(tween(280ms))corriendo en paralelo- Re-layout del
LazyColumnen cada frame durante280ms
Dónde ocurre:
ArtistDetailScreen.kt:358-377— cadaArtistAlbumSectionSongItemwrapeado- Probablemente
GenreDetailScreen.ktcon patrón similar
Síntomas: Frame drops visibles (jank) al expandir/colapsar secciones de artistas, especialmente con artistas que tienen muchos álbumes/canciones. En gama baja podría freezar ~500ms.
Evidencia directa: Código confirmado en lectura de ArtistDetailScreen.kt:339-377.
Severidad: ALTA
Impacto real: EnhancedSongListItem ejecuta simultáneamente:
animateDpAsState(corner radius) —400msanimateDpAsState(album corner radius)animateFloatAsState(selection scale) — springanimateDpAsState(border width) —250msanimateColorAsState(container color) —300msanimateColorAsState(content color) —300msanimateColorAsState(border color) —250ms
Cada una crea un Animatable con su propio frame callback. Cuando cambia currentSong (el usuario pasa a la siguiente canción), las 7 animaciones se disparan en el item anterior y las 7 en el nuevo item = 14 animaciones simultáneas + la animación de transición del player.
Dónde: EnhancedSongListItem.kt:97-166, usado en:
DailyMixSection(Home screen, hasta 4 items)LibrarySongsTab(potencialmente visibles en pantalla~8-12items)PlaylistDetailScreen
Síntomas: Micro-jank al cambiar de canción, especialmente perceptible si el usuario está scrolleando la lista de canciones al mismo tiempo.
Severidad: ALTA
Impacto real: LibraryStateHolder.kt:134-174 mapea todas las canciones a géneros cada vez que _allSongs emite. Incluye:
- Iteración de toda la colección de canciones
mutableMapOf + getOrPutpara agruparmapIndexedNotNullpara transformarGenreThemeUtils.getGenreThemeColor()por cada género (light + dark)- Conversión de color a hex string por género
distinctBy,sortedBy,toImmutableList()
Mitigación parcial: Usa .flowOn(Dispatchers.Default) — no bloquea main thread. Pero:
- Emite una nueva
ImmutableList<Genre>incluso si los géneros no cambiaron - No tiene
distinctUntilChanged()antes delmap(elmapse re-ejecuta aunque las canciones sean idénticas) - Para
10,000canciones con50géneros, esto es~10,000iteraciones +100llamadas agetGenreThemeColor()+ sort
Evidencia: Líneas 134-174 de LibraryStateHolder.kt confirmadas por lectura directa. No hay distinctUntilChanged() en la chain.
Severidad: ALTA
Impacto real: 32 invocaciones de .toImmutableList() en el layer de ViewModels. Para operaciones de cola (queue):
- Eliminar 1 canción de una cola de
1000=filter { }.toImmutableList()→ itera1000items + copia persistente - Cada cambio de cola actualiza
PlayerUiState.currentPlaybackQueue, que esImmutableList<Song> Songes undata classcon~20campos incluyendo strings
Dónde:
PlayerViewModel.kt—9ocurrencias directasLibraryStateHolder.kt—10ocurrenciasDailyMixStateHolder.kt—6ocurrencias
Síntomas: Presión de GC intermitente. En gama baja con menor heap, el GC se dispara más frecuentemente, causando micro-pausas de 2-8ms que se acumulan con otras operaciones.
Severidad: MEDIA
Impacto real: getSongByPath() query hace full table scan. Con 10,000+ canciones, esto puede tomar 10-50ms en gama baja. Es llamado durante sync y potencialmente durante resolución de URIs.
Dónde: MusicDao.kt — @Query("SELECT * FROM songs WHERE file_path = :path LIMIT 1")
Evidencia: Análisis de SongEntity.kt muestra índices en title, album_id, artist_id, genre, parent_directory_path, content_uri_string, date_added, duration, source_type, artist_name — pero no en file_path.
Severidad: MEDIA-ALTA
Impacto real: getAllSongs(), getAlbums(), getArtists() en MusicDao.kt devuelven Flow<List<Entity>> sin LIMIT. Con 50,000 canciones:
getAllSongs()→50,000 SongEntityen memoria (~50MB+dependiendo del tamaño de strings)getAlbums()conGROUP BY + JOIN→ miles de albumsgetArtists()con dobleJOIN (cross_ref)→ potencialmente miles
Estos flows se re-emiten cuando las tablas subyacentes cambian (Room's invalidation tracker).
Mitigación parcial: Ya hay Paging3 para songsPagingFlow y favoritesPagingFlow. Pero allSongs, albums, artists todavía cargan completos y se mantienen en StateFlow<ImmutableList<...>> en LibraryStateHolder.
Severidad: MEDIA
Impacto real: El campo currentPosition: Long vive dentro de PlayerUiState (línea 19). Aunque existe un flow separado currentPlaybackPosition en PlaybackStateHolder, si el _playerUiState también se actualiza con la posición, cada tick de 250ms invalida todo el PlayerUiState.
Hipótesis parcial: Necesito confirmar si _playerUiState.update { it.copy(currentPosition = ...) } se llama cada 250ms. Si es así, la severidad sube a CRÍTICA dado que amplifica el Hallazgo 1. Si currentPosition en PlayerUiState solo se actualiza en seek/song change, es menos grave.
Evidencia:
- El campo existe en
PlayerUiState.kt:19 - El flow separado existe en
PlaybackStateHolder.kt:60-61 - Falta trazar si el update de
_playerUiState.currentPositiones cada tick o solo en eventos discretos
PlayerUiState tiene ~30 campos que cubren queue, search, folders, undo bars, sort options, filter state, sync state, AI state y más. Es un MutableStateFlow con updates imperativos. Cada .update { copy(...) } emite a todos los suscriptores, de los cuales hay al menos 3 que no usan slicing (LibraryAlbumsTab, LibraryArtistsTab, SearchScreen). Esto crea un fan-out de recomposición donde un cambio en un campo irrelevante (ej: searchQuery) puede forzar recomposición de AlbumsTab.
PlayerViewModel inyecta 15+ StateHolders, todos @Singleton. LibraryStateHolder mantiene allSongs, albums, artists en memoria permanente como StateFlow. Esto significa que:
- La memoria baseline de la app crece linealmente con el tamaño de la biblioteca y nunca se libera
- No hay mecanismo de
onTrimMemory()para liberar colecciones en presión de memoria - En dispositivos con
3-4GB RAM, esto puede forzar GC agresivo o incluso OOM con bibliotecas muy grandes
Muchos composables reciben playerViewModel: PlayerViewModel directamente como parámetro. Dentro del composable, hacen playerViewModel.playerUiState.collectAsStateWithLifecycle(). Este patrón significa que el scope de observación es el composable entero —no un sub-composable aislado—. Cualquier emisión de playerUiState recompone todo el composable y todos sus hijos.
32 .toImmutableList() en ViewModels + conversiones en DAOs/Repositories. kotlinx.collections.immutable usa persistent data structures (basados en Hash Array Mapped Tries), cuya construcción desde listas mutables es O(n). Para la cola de reproducción (que puede cambiar frecuentemente durante shuffle/skip/remove), esto genera allocations significativas que el GC debe recoger.
- Recomposición de tabs de librería cada
250msdurante playback — por observación completa dePlayerUiStateque incluyecurrentPosition - Frame drops al expandir secciones de artista — por
AnimatedVisibilitypor-item con50+items simultáneos - Micro-stutters en scroll de listas largas — por 7 animaciones paralelas por
EnhancedSongListItem+ falta decontentTypeen algunosLazyList
- Jank al cambiar tabs en Library — la tab nueva observa
playerUiStatey recompone completamente - Stutters al abrir/cerrar
QueueBottomSheetcon colas grandes — key function hace4x getOrNullpor item + reorder preview logic - GC pauses en gama baja — por acumulación de
toImmutableList + allSongsen memoria + color scheme bitmaps - Computación de géneros innecesaria —
O(n)sobre todas las canciones sindistinctUntilChanged
- Position ticker actualizando
PlayerUiStatecada250ms— confirmar si_playerUiState.update { copy(currentPosition = ...) }se llama en cada tick - Prefetch de imágenes de artistas bloqueando brevemente main thread — depende de la implementación de
prefetchArtistImages - Room invalidation tracker re-emitiendo flows grandes después de sync — Room re-emite toda la tabla cuando cualquier row cambia
- Color palette extraction durante transitions rápidas — aunque tiene capacity
DROP_OLDEST, el procesamiento de bitmap ocurre enDefault dispatcherque comparte threads con animation dispatching
Ordenados de mayor a menor prioridad:
| # | Componente | Problema principal | Frecuencia de impacto |
|---|---|---|---|
| 1 | LibraryAlbumsTab / LibraryArtistsTab |
Observan playerUiState completo |
Constante durante playback |
| 2 | ArtistDetailScreen secciones |
AnimatedVisibility por item |
Cada expand/collapse |
| 3 | EnhancedSongListItem |
7 animaciones por item | Cada cambio de canción |
| 4 | QueueBottomSheet |
Key computation + reorder logic con colas 1000+ |
Cada apertura/interacción |
| 5 | SearchScreen |
Observa playerUiState completo |
Constante durante playback |
| 6 | LibraryStateHolder.genres |
O(n) sin deduplicación |
Cada cambio en allSongs |
| 7 | PlayerUiState updates |
Monolithic state → fan-out | Cada 250ms (si incluye posición) |
| 8 | PlaylistDetailScreen |
Carga completa sin paginación + sort in-memory | Playlists con 500+ canciones |
| 9 | DailyMixSection |
forEach sin LazyColumn (4 items con 7 anim cada uno) |
Home screen render |
| 10 | MusicDao queries sin LIMIT |
Datasets completos en memoria | Cada invalidación de tabla |
Un Snapdragon 680 tiene ~50% del throughput de un Snapdragon 8 Gen 2. El frame budget sigue siendo 16ms, pero la capacidad de procesamiento por frame es la mitad. Esto significa:
- Las
14animaciones simultáneas por cambio de canción (7 old + 7 new item) que en un flagship toman3ms, en gama media toman6-8ms— dejando solo8mspara layout, draw y todo lo demás - Las recomposiciones innecesarias por
playerUiStateque en un flagship son "invisibles" (2-3ms), en gama media consumen5-8ms— cerca del50%del frame budget
AnimatedVisibilityconexpandVerticallyfuerza re-layout delLazyColumn. En GPUs modestas (Adreno 610,Mali-G57), el re-rasterizado de50+items simultáneamente puede causar GPU-bound jank- Hardware bitmaps para album art prefetch consumen GPU memory que es más limitada en gama baja
4GB RAM con ~1.5GB disponible para la app. Si allSongs tiene 10,000 Song objects (~20 campos cada uno, estimado ~1KB por Song) = ~10MB solo en allSongs. Sumando albums, artists, queue, search results, color schemes, Coil cache (20% heap ≈ 50MB) → la app puede estar usando 100-150MB base.
En 3GB devices, esto es ~10% de RAM total. GC generacional de ART se activa más frecuentemente → micro-pauses de 1-5ms cada few seconds.
Las conversiones .toImmutableList() crean copias efímeras que amplifican la presión de GC.
Room usa CursorWindow de 2MB por defecto. Con 10,000+ canciones, los campos String (title, artist_name, album, file_path, content_uri_string) pueden exceder el window.
Nota positiva: ya hay SONG_LIST_PROJECTION que excluye lyrics para prevenir overflow.
allSongs StateFlowmantiene20,000 Song objectspermanentemente →~20MBsolo en esa coleccióngenresse recomputa:20,000iteraciones +groupBy→ potencialmente200+géneros con theme colors- Room queries sin
LIMITdevuelven datasets enormes →CursorWindowpressure - Shuffle de
20,000items (Fisher-Yates) →O(n)pero sin dispatching forzoso aDefaultenQueueStateHolderbase
Predicción: Posible ANR en sync; jank severo en genre tab; OOM en devices con 3GB RAM.
- Coil con
20% memory + 100MB diskes razonable - Pero sin
onTrimMemory()callback para evict, bajo presión de memoria el sistema mata la app antes de que Coil limpie su cache ColorSchemeProcessorLRUde30 entries + 128x128 bitmaps=~2MB— manejable
Predicción: Estable en uso normal. En low-memory situations, posible kill por sistema.
PlaylistViewModel.loadPlaylistDetails()carga todas las canciones de una playlist en memoria- Playlists con
2000+canciones:getSongsByIds()divide en chunks de900, pero el resultado final es una solaList<Song>en memoria - Sort se aplica sobre la lista completa in-memory
Predicción: Jank de 200-500ms al abrir playlists con 2000+ canciones en gama baja.
- Search debounce a
300mses razonable - Pero
SearchScreenobservaplayerUiStatecompleto → recompone durante playback - Sort change en albums/artists tab fuerza
scrollToItem(0)+ recomposición completa del grid/list
Predicción: Search es relativamente smooth gracias al debounce. Sort changes causan breve flash visual.
- Position ticker cada
250msestá bien aislado encurrentPositionseparado - Pero si también actualiza
PlayerUiState.currentPosition, el fan-out es masivo LavaLampColors(animación en player) actualizaPlayerUiStatesi cambia
Predicción: El mayor riesgo es la hipótesis #8 — necesita validación.
- Navigation transitions de
250msconFastOutSlowInEasing - Cada pantalla crea nuevos
collectAsStateWithLifecycle()subscriptions WhileSubscribed(5000)=5segundos de gracia antes de cancelar la suscripción. Si el usuario navega rápido (< 5spor pantalla), las suscripciones de pantallas previas siguen activas → múltiples flows activos simultáneamentenavigateSafely()tiene retry logic — puede encolar múltiples navigations
Predicción: Potencial para buildup de suscripciones activas durante navegación rápida. Efecto moderado.
- Player sheet usa
MutatorMutexpara prevenir conflictos de animación — bien diseñado - Layout-phase reads con
Modifier.layoutpara drag — excelente optimización graphicsLayerlambdas conAnimatable getters— evita recomposición- Queue sheet + player sheet + lyrics sheet potencialmente abiertos con animaciones concurrentes
Predicción: La implementación del player sheet es sólida. El riesgo mayor es QueueBottomSheet con colas grandes y su lógica de reorder preview.
Reemplazar playerViewModel.playerUiState.collectAsStateWithLifecycle() con:
val albumsTabSlice by remember {
playerViewModel.playerUiState
.map { AlbumsTabSlice(it.isAlbumsListView, it.currentAlbumSortOption) }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = AlbumsTabSlice())Impacto esperado: Elimina recomposiciones innecesarias de 3 screens. Riesgo: Muy bajo — solo cambia cómo se observa, no qué se observa. Preservar: Comportamiento de sort reset y fast scroll label.
En LibraryStateHolder.kt:134, agregar antes del .map:
_allSongs.distinctUntilChanged().map { songs -> ... }O mejor: agregar después del map con comparación por IDs de géneros.
Impacto: Evita recomputación si allSongs se re-emite con datos idénticos.
Riesgo: Mínimo.
En ArtistDetailScreen.kt:353-377, cambiar la visibilidad de items en LazyColumn. Usar la list size condicionada (ya existe isExpanded) + Modifier.animateItem() de LazyColumn.
Impacto: Reduce de N animaciones simultáneas a animación de layout gestionada por LazyColumn.
Riesgo: Cambio visual en la animación de expand/collapse — puede necesitar ajuste de timing.
Preservar: Sensación de expand/collapse suave.
@ColumnInfo(index = true) en el campo file_path, o migration con CREATE INDEX.
Impacto: getSongByPath() pasa de full scan a O(log n) lookup.
Riesgo: Requiere migration de DB (incrementar versión).
Reemplazar 7 animateXAsState individuales con un solo updateTransition que coordine todos los valores.
Impacto: Reduce overhead de 7 Animatables independientes a 1 transición coordinada.
Riesgo: Necesita testing visual cuidadoso para asegurar que los timings se preserven.
Preservar: La sensación de cambio suave en selection y current song highlight.
Separar en: QueueUiState, SearchUiState, FolderUiState, LibraryPrefsUiState. Cada uno como StateFlow independiente.
Impacto: Elimina cross-contamination de updates entre funcionalidades.
Riesgo: Moderado — requiere cambios en múltiples screens que leen PlayerUiState.
Preservar: API pública de PlayerViewModel (puede delegarse internamente).
Registrar ComponentCallbacks2 en Application para responder a onTrimMemory():
- Limpiar
LibraryStateHolder.allSongs - Limpiar
Coil memory cache - Limpiar
color scheme cachebajo presión - Recargar desde DB/paging cuando se vuelvan a necesitar
Impacto: Reduce probabilidad de OOM/kill en gama baja. Riesgo: Necesita manejar correctamente el estado "vacío después de trim".
- Pre-computar activeKeys map fuera del
items() block - Evitar
4x getOrNullpor item — usar indexación directa con bounds check una sola vez
Impacto: Reduce overhead per-frame de queue rendering. Riesgo: Bajo.
Eliminar StateFlow<ImmutableList<Song>> en LibraryStateHolder. Usar Paging3 para todos los consumidores de la lista completa.
Impacto: Elimina el problema de memoria O(n) con el tamaño de biblioteca.
Riesgo: Alto — afecta genres computation, daily mix, search, stats, AI playlist generation, y cualquier flujo que dependa de allSongs.
Preservar: Genres todavía necesitan datos completos (considerar query dedicada en DB).
Alternativa: Mantener allSongs pero con deferred loading + WeakReference.
Actualmente es un God Object de 4,631 líneas con 15 StateHolders. Considerar exponer StateHolders directamente a UI a través de CompositionLocals o Hilt-injected ViewModels por screen.
Impacto: Reduce acoplamiento, permite lifecycle management por screen.
Riesgo: Muy alto — cambio arquitectónico masivo.
Preservar: El patrón de slicing ya funcional (fullPlayerSlice, playerConfigSlice).
- Agregar índices compuestos para queries frecuentes
- Implementar
LIMIT + offseten queries de lista - Considerar
Room Multimap return typespara reducirJOINs
Impacto: Mejora tiempos de query en bibliotecas grandes. Riesgo: Medio — migrations + testing de data integrity.
- Fase 1: Prácticamente sin riesgo funcional. Solo observación y animation changes.
- Fase 2: Riesgo moderado en
2.2(desacoplarPlayerUiState) — muchos consumidores que actualizar. - Fase 3: Riesgo alto.
3.1y3.2son cambios estructurales que pueden introducir regresiones si no se testean exhaustivamente.
- Transición suave del mini player al full player (actualmente bien implementada con layout-phase reads)
- Crossfade de album art (
350mscon Coil nativo) - Animación de lava lamp colors en player
- Expand/collapse de secciones de artista (puede cambiar técnica pero debe verse fluido)
- Selection mode animations (puede consolidarse pero debe mantenerse feedback visual)
- Haptic feedback system-wide
- Queue drag-to-reorder visual preview
Herramienta: Layout Inspector (Android Studio) + Recomposition Counts overlay
Método: Activar "Show recomposition counts" en Compose debugging. Navegar al tab de Albums con música reproduciendo. Observar si el recomposition count incrementa cada 250ms.
- Señal confirmatoria: Count incrementando continuamente sin interacción del usuario
- Señal descartante: Count estable mientras no hay interacción
Herramienta: System Tracing (Perfetto) o GPU Rendering Profile (Developer Options)
Método: Abrir ArtistDetailScreen con un artista que tenga 20+ canciones en una sección. Expand/collapse la sección. Capturar trace.
- Señal confirmatoria: Frames
> 16msdurante la animación, conChoreographer#doFramemostrando layout/measure spikes - Señal descartante: Frames dentro de budget
Herramienta: Grep + breakpoint condicional
Método: Buscar todas las llamadas a _playerUiState.update { it.copy(currentPosition = ...) } en PlayerViewModel.kt
- Señal confirmatoria: Update llamado dentro del progress loop o con frecuencia alta
- Señal descartante: Solo llamado en seek events o song transitions
Herramienta: Macrobenchmark con FrameTimingMetric
Tests:
- Scroll performance en
LibrarySongsTabcon5000canciones y música reproduciendo - Tab switch timing (
Home → Library → Search cycle) ArtistDetailScreensection expand con50+cancionesQueueBottomSheetopen + scroll con cola de500canciones
Baseline device: Android emulator con performance throttled, o device físico gama media (ej: Pixel 4a, Samsung A53)
Herramienta: Android Studio Profiler → Memory tab
Método: Navegar por la app durante 5 minutos con biblioteca de 10,000+ canciones. Observar heap allocation rate y GC events.
- Señal confirmatoria: GC events cada
< 5segundos, heap growing monotónicamente,> 150MB allocation - Señal descartante: GC events esporádicos, heap estable después de initial load
Herramienta: Macrobenchmark con StartupTimingMetric
Tests: Cold start → Home screen rendered → Library tab rendered
Complementar con: Perfetto trace del primer SyncWorker.doWork() pass
Habilitar vía gradle property:
pixelplayer.enableComposeCompilerReports=trueRevisar:
- Número de composables
restartablevsskippable - Buscar composables con parámetros marcados como
UNSTABLEque deberían ser estables
Particularmente: verificar que Song, Album, Artist, Playlist son efectivamente estables (ya están en compose_stability.conf, pero validar).
Ratio impacto/esfuerzo: Máximo
- Unas pocas líneas de código por screen eliminan el mayor vector de recomposición innecesaria
- Efecto inmediato en scroll smoothness durante playback
Ratio impacto/esfuerzo: Muy alto
- Afecta directamente la experiencia en pantallas de detalle de artista/género
- Cambio localizado, testeable de forma aislada
Ratio impacto/esfuerzo: Alto
- Reduce overhead per-frame en todas las listas de canciones
- Si se confirma: es la fix de mayor impacto posible (elimina
4 emisiones/segundode un state masivo) - Si se descarta: se cierra la hipótesis y se priorizan otros items
Wins garantizados con riesgo mínimo.
Crítico para retención de usuarios en gama baja. Sin esto, la app es vulnerable a kills en background.
El refactor que previene que futuros features degraden la performance. Hacerlo después de los slices individuales (1.1) que ya habrán mitigado el problema inmediato.
Empezar por eliminar trabajo innecesario (recomposiciones fantasma, animaciones excesivas) antes de optimizar trabajo necesario (queries, data structures). El mayor gain de fluidez percibida viene de eliminar lo que nunca debería haberse ejecutado.