0.2.85
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
# Home performance baseline
|
||||||
|
|
||||||
|
This is the repeatable performance check for Home. The source baseline is the current
|
||||||
|
good-performing release line at commit `5467fba` (0.2.84). Numbers are only comparable on
|
||||||
|
the same television, Android build, server, network and signed-in profile, so benchmark
|
||||||
|
results belong with the test run or release notes rather than as universal targets here.
|
||||||
|
|
||||||
|
## What is measured
|
||||||
|
|
||||||
|
- `coldStartToHomeNoCompilation`: cold launch without ahead-of-time compilation, used as
|
||||||
|
the control for the baseline profile.
|
||||||
|
- `coldStartToHomeWithProfile`: cold launch as a release is shipped. `timeToFullDisplayMs`
|
||||||
|
ends when Home reports its first D-pad-ready, settled frame.
|
||||||
|
- `warmStartToHomeWithProfile`: returning to a resident process.
|
||||||
|
- `homeDpadAndRows`: frame timing during rapid travel across cards, down four rows, and back
|
||||||
|
through already displayed artwork. The route deliberately does not idle between presses.
|
||||||
|
- Debug milestones in `MembyStartup`: process start to `home_visible`,
|
||||||
|
`first_row_visible`, and `home_interactive`.
|
||||||
|
- Debug frame windows in `EmbyClientPerf`: jank percentage, p50/p95/p99 UI frame time,
|
||||||
|
frames over 16 ms and 33 ms, and the maximum frame time.
|
||||||
|
|
||||||
|
Compose Layout Inspector supplies card and row recomposition counts. Capture a system trace
|
||||||
|
at the same time when a frame regression needs attribution; the trace shows Compose work,
|
||||||
|
Coil fetch/decode activity and main-thread scheduling together.
|
||||||
|
|
||||||
|
## Run protocol
|
||||||
|
|
||||||
|
Use a physical TV with a signed-in profile and a populated Home cache. Keep the TV's display
|
||||||
|
mode, server, network and Home row preferences unchanged between the reference and candidate
|
||||||
|
runs.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:JAVA_HOME = "C:\Program Files\Android\Android Studio\jbr"
|
||||||
|
.\gradlew.bat :benchmark:connectedBenchmarkReleaseAndroidTest `
|
||||||
|
-P android.testInstrumentationRunnerArguments.class=com.ponzischeme89.memby.benchmark.HomeBenchmark
|
||||||
|
```
|
||||||
|
|
||||||
|
For an interactive debug capture, clear logcat, cold-start Memby, wait for Home, then rapidly
|
||||||
|
hold Right and move through at least four rows. Record:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
adb logcat -s MembyStartup EmbyClientPerf
|
||||||
|
```
|
||||||
|
|
||||||
|
In Layout Inspector, enable recomposition counts and repeat the same route. Unchanged cards
|
||||||
|
outside the old and new focus targets should normally be skipped; focus animation should not
|
||||||
|
cause per-frame card recomposition. In Android Studio's system trace, inspect the slowest
|
||||||
|
frames for main-thread Compose work and Coil image fetch/decode slices.
|
||||||
|
|
||||||
|
## Regression gates
|
||||||
|
|
||||||
|
Use the median of at least five iterations. Investigate before release when, against a fresh
|
||||||
|
reference run on the same setup:
|
||||||
|
|
||||||
|
- profiled cold-start `timeToFullDisplayMs` or warm start regresses by more than 10%;
|
||||||
|
- `frameDurationCpuMs` p95 regresses by more than 10%, or p99 crosses 33 ms;
|
||||||
|
- a previously displayed card causes a network fetch or another decode on the return pass;
|
||||||
|
- focus animation recomposes a card on every animation frame or invalidates unaffected cards;
|
||||||
|
- adding a Home row materially changes first-row visibility time. Below-the-fold rows may
|
||||||
|
affect `home_interactive`, but must not delay `first_row_visible`.
|
||||||
|
|
||||||
|
Regenerate the baseline profile after changing the startup or first-browse path:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat :app:generateReleaseBaselineProfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the reference and candidate benchmark result directories together. Hardware variance
|
||||||
|
makes the paired result the baseline; a number copied from a different TV is not evidence of
|
||||||
|
a regression or an improvement.
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -63,7 +64,7 @@ val projectNoticeText =
|
|||||||
|
|
||||||
// A release workflow can derive the app version from its Git tag without editing the
|
// A release workflow can derive the app version from its Git tag without editing the
|
||||||
// source tree. Local builds keep using the checked-in default.
|
// source tree. Local builds keep using the checked-in default.
|
||||||
val defaultVersionName = "0.2.84"
|
val defaultVersionName = "0.2.85"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
@@ -95,7 +96,7 @@ fun environmentSecret(key: String): String? {
|
|||||||
return System.getenv(key)?.trim()?.takeIf { it.isNotEmpty() }
|
return System.getenv(key)?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
extensions.configure<ApplicationExtension> {
|
||||||
namespace = "com.ponzischeme89.memby"
|
namespace = "com.ponzischeme89.memby"
|
||||||
// Raised to 37 by the Lifecycle 2.11 / Activity 1.13 upgrade, which refuse to be
|
// Raised to 37 by the Lifecycle 2.11 / Activity 1.13 upgrade, which refuse to be
|
||||||
// consumed by a project compiled against anything older. This is a *compile* target
|
// consumed by a project compiled against anything older. This is a *compile* target
|
||||||
|
|||||||
@@ -1687,7 +1687,7 @@ internal fun MediaRow(
|
|||||||
onVerticalFocusRequestConsumed: (Int) -> Unit,
|
onVerticalFocusRequestConsumed: (Int) -> Unit,
|
||||||
onMoveVertical: (itemIndex: Int, direction: RowFocusDirection) -> Boolean,
|
onMoveVertical: (itemIndex: Int, direction: RowFocusDirection) -> Boolean,
|
||||||
onContentFocused: () -> Unit,
|
onContentFocused: () -> Unit,
|
||||||
onItemFocused: (BaseItem) -> Unit,
|
onItemFocused: (item: BaseItem, itemIndex: Int) -> Unit,
|
||||||
onItemSelected: (BaseItem) -> Unit,
|
onItemSelected: (BaseItem) -> Unit,
|
||||||
onItemLongPressed: (BaseItem) -> Unit,
|
onItemLongPressed: (BaseItem) -> Unit,
|
||||||
density: String = "standard",
|
density: String = "standard",
|
||||||
@@ -1840,7 +1840,9 @@ internal fun MediaRow(
|
|||||||
}
|
}
|
||||||
val focused: () -> Unit = {
|
val focused: () -> Unit = {
|
||||||
onContentFocused()
|
onContentFocused()
|
||||||
onItemFocused(item)
|
// LazyRow already knows the semantic position. Passing it on
|
||||||
|
// avoids searching the row again on every D-pad focus move.
|
||||||
|
onItemFocused(item, index)
|
||||||
}
|
}
|
||||||
val format = cardFormat(row.kind, item, artworkStyle)
|
val format = cardFormat(row.kind, item, artworkStyle)
|
||||||
if (row.kind == MediaRowKind.CONTINUE) {
|
if (row.kind == MediaRowKind.CONTINUE) {
|
||||||
|
|||||||
@@ -1593,9 +1593,8 @@ internal fun HomeScreen(
|
|||||||
true
|
true
|
||||||
},
|
},
|
||||||
onContentFocused = { navigationExpanded = false },
|
onContentFocused = { navigationExpanded = false },
|
||||||
onItemFocused = { item ->
|
onItemFocused = { item, itemIndex ->
|
||||||
val itemIndex = row.items.indexOfFirst { it.id == item.id }
|
rowFocusPositions[row.id] = itemIndex
|
||||||
if (itemIndex >= 0) rowFocusPositions[row.id] = itemIndex
|
|
||||||
rowFocusedBelowHero = true
|
rowFocusedBelowHero = true
|
||||||
if (selectedDestination == BrowseDestination.HOME) {
|
if (selectedDestination == BrowseDestination.HOME) {
|
||||||
focusedHomeRowId = row.id
|
focusedHomeRowId = row.id
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ internal object LaunchIntro {
|
|||||||
* - **The player is returned, not released.** It goes back to the process cache on dispose,
|
* - **The player is returned, not released.** It goes back to the process cache on dispose,
|
||||||
* which is what lets the very next playback still open on a prepared instance.
|
* which is what lets the very next playback still open on a prepared instance.
|
||||||
*/
|
*/
|
||||||
@OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(markerClass = [UnstableApi::class])
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LaunchPrerollVideo(
|
internal fun LaunchPrerollVideo(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.ponzischeme89.memby.R
|
|||||||
* sought back to the beginning and prepared again after use rather than reconstructed for
|
* sought back to the beginning and prepared again after use rather than reconstructed for
|
||||||
* every title.
|
* every title.
|
||||||
*/
|
*/
|
||||||
@OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(markerClass = [UnstableApi::class])
|
||||||
internal object PrerollPreloader {
|
internal object PrerollPreloader {
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private var applicationContext: Context? = null
|
private var applicationContext: Context? = null
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ponzischeme89.memby.ui
|
package com.ponzischeme89.memby.ui
|
||||||
|
|
||||||
import com.ponzischeme89.memby.data.HomeCache
|
import com.ponzischeme89.memby.data.HomeCache
|
||||||
|
import com.ponzischeme89.memby.data.PlaybackPosition
|
||||||
import com.ponzischeme89.memby.data.model.BaseItem
|
import com.ponzischeme89.memby.data.model.BaseItem
|
||||||
import com.ponzischeme89.memby.data.model.HomeRow
|
import com.ponzischeme89.memby.data.model.HomeRow
|
||||||
import com.ponzischeme89.memby.data.model.UserItemData
|
import com.ponzischeme89.memby.data.model.UserItemData
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class ResumableMediaCardTest {
|
|||||||
@Test
|
@Test
|
||||||
fun finishTimeUsesRemainingRuntime() {
|
fun finishTimeUsesRemainingRuntime() {
|
||||||
val now = 1_000_000L
|
val now = 1_000_000L
|
||||||
val runtime = 60L * 60L * TICKS_PER_MILLISECOND_FOR_TEST
|
val runtime = 60L * 60L * MILLIS_PER_SECOND * TICKS_PER_MILLISECOND_FOR_TEST
|
||||||
val position = 15L * 60L * TICKS_PER_MILLISECOND_FOR_TEST
|
val position = 15L * 60L * MILLIS_PER_SECOND * TICKS_PER_MILLISECOND_FOR_TEST
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
now + 45L * 60L * 1_000L,
|
now + 45L * 60L * 1_000L,
|
||||||
@@ -61,6 +61,7 @@ class ResumableMediaCardTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
const val MILLIS_PER_SECOND = 1_000L
|
||||||
const val TICKS_PER_MILLISECOND_FOR_TEST = 10_000L
|
const val TICKS_PER_MILLISECOND_FOR_TEST = 10_000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,32 @@ class HomeBenchmark {
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun MacrobenchmarkScope.exerciseHome() {
|
private fun MacrobenchmarkScope.exerciseHome() {
|
||||||
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_RIGHT)
|
// Do not wait between presses: a real performance problem is most visible while a
|
||||||
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_RIGHT)
|
// viewer holds the remote and focus animations, lazy composition and image decode
|
||||||
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_DOWN)
|
// overlap. Travelling both ways also covers cached artwork and item reuse rather
|
||||||
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_RIGHT)
|
// than measuring only the first decode of each card.
|
||||||
|
repeat(HORIZONTAL_PRESSES) {
|
||||||
|
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||||
|
}
|
||||||
|
repeat(VERTICAL_PRESSES) {
|
||||||
|
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_DOWN)
|
||||||
|
repeat(HORIZONTAL_PRESSES_PER_ROW) {
|
||||||
|
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_RIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeat(VERTICAL_PRESSES) {
|
||||||
|
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_UP)
|
||||||
|
repeat(HORIZONTAL_PRESSES_PER_ROW) {
|
||||||
|
device.pressKeyCode(android.view.KeyEvent.KEYCODE_DPAD_LEFT)
|
||||||
|
}
|
||||||
|
}
|
||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val PACKAGE_NAME = "com.ponzischeme89.memby"
|
const val PACKAGE_NAME = "com.ponzischeme89.memby"
|
||||||
|
const val HORIZONTAL_PRESSES = 12
|
||||||
|
const val HORIZONTAL_PRESSES_PER_ROW = 6
|
||||||
|
const val VERTICAL_PRESSES = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user