Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions androidApp/benchmark-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Proguard rules for the `benchmark` build type.
#
# Obsfuscation must be disabled for the build variant that generates Baseline Profile, otherwise
# wrong symbols would be generated. The generated Baseline Profile will be properly applied when generated
# without obfuscation and your app is being obfuscated.
-dontobfuscate
14 changes: 14 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.util.Properties
@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.baseline.profile)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.ludi.common)
Expand Down Expand Up @@ -48,6 +49,11 @@ android {
signingConfig = signingConfigs.getByName("release")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
create("benchmark") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
}
}

kotlinOptions {
Expand Down Expand Up @@ -96,6 +102,12 @@ appVersioning {
}
}

baselineProfile {
automaticGenerationDuringBuild = true
saveInSrc = true
mergeIntoMain = true
}

dependencies {
// Core Android dependencies
implementation(libs.androidx.core.ktx)
Expand All @@ -104,6 +116,8 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.splash.screen)
implementation(libs.profiler.installer)
baselineProfile(project(":benchmark"))

implementation(project(":shared"))

Expand Down
5 changes: 5 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
android:theme="@style/Theme.Ludi.Splash"
android:name=".LudiApplication"
tools:targetApi="31">

<profileable
android:shell="true"
tools:targetApi="29" />

<!--Required for robolectric tests, otherwise tests fail with RuntimeException("Unable to resolve activity"),
see https://github.com/robolectric/robolectric/pull/4736 for details-->
<activity
Expand Down
10 changes: 9 additions & 1 deletion androidApp/src/main/kotlin/com/mr3y/ludi/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
Expand All @@ -29,6 +33,7 @@ class MainActivity : ComponentActivity(), HostActivityComponentOwner {
HostActivityComponent::class.create(this, AndroidApplicationComponent.from(this))
}

@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -75,7 +80,10 @@ class MainActivity : ComponentActivity(), HostActivityComponentOwner {
}
},
useDynamicColor = userPreferences!!.useDynamicColor,
showOnboardingScreen = userPreferences!!.showOnBoardingScreen
showOnboardingScreen = userPreferences!!.showOnBoardingScreen,
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
)
}
}
Expand Down
1 change: 1 addition & 0 deletions benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
56 changes: 56 additions & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import com.android.build.api.dsl.ManagedVirtualDevice

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.baseline.profile)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ludi.common)
alias(libs.plugins.ludi.android.common)
}

android {
namespace = "com.mr3y.ludi.benchmark"

kotlinOptions {
jvmTarget = "17"
}

defaultConfig {
targetSdk = 33
}

buildFeatures {
compose = false
aidl = false
buildConfig = false
renderScript = false
shaders = false
}

testOptions {
managedDevices {
devices {
create<ManagedVirtualDevice>("pixel7Api33") {
device = "Pixel 7"
apiLevel = 33
systemImageSource = "aosp-atd"
}
}
}
}

targetProjectPath = ":androidApp"
}

baselineProfile {
managedDevices += "pixel7Api33"
useConnectedDevices = false
}

dependencies {
implementation(libs.androidx.test.ext.junit)
implementation(libs.espresso.core)
implementation(libs.uiautomator)
implementation(libs.benchmark.macro.junit4)
}
1 change: 1 addition & 0 deletions benchmark/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.mr3y.ludi.benchmark

import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()

@Test
fun generate() = baselineProfileRule.collect(
packageName = "com.mr3y.ludi",
stableIterations = 2,
maxIterations = 10,
profileBlock = {
// Start the app
pressHome()
startActivityAndWait()

// simulate user journey through the app.
waitForGenresToLoad()
selectRandomGenres()
moveToNextPage()

waitForGamesToLoad()
selectRandomGames()
scrollGamesContainerHorizontally()
moveToNextPage()

waitForDataSources()
moveToNextPage()
}
)

fun MacrobenchmarkScope.waitForGenresToLoad() {
device.wait(Until.gone(By.res("onboarding:genres:loadingWheel")), 10_000L)
device.wait(Until.hasObject(By.res("onboarding:genres:content")), 1000L)
}

fun MacrobenchmarkScope.selectRandomGenres() {
val genres = device.findObject(By.res("onboarding:genres:content")).children
var i = 0
while (i < 3) {
val genre = genres.random()
if (genre.isSelected) {
continue
}
genre.click()
device.waitForIdle()
i++
}
}

fun MacrobenchmarkScope.waitForGamesToLoad() {
device.wait(Until.gone(By.res("onboarding:games:gameLoading")), 10_000L)
device.wait(Until.hasObject(By.res("onboarding:games:gameContent")), 1000L)
}

fun MacrobenchmarkScope.selectRandomGames() {
val gamesContainer = device.waitForObject(By.res("onboarding:games:suggestedGames"))

// Set gesture margin from sides not to trigger system gesture navigation
val horizontalMargin = 10 * gamesContainer.visibleBounds.width() / 100
gamesContainer.setGestureMargins(horizontalMargin, 0, horizontalMargin, 0)
val games = gamesContainer.children.filter { it.resourceName == "onboarding:games:gameContent" }
repeat(3) {
games.random().click()
device.waitForIdle()
}
}

fun MacrobenchmarkScope.scrollGamesContainerHorizontally() {
val gamesContainer = device.waitForObject(By.res("onboarding:games:suggestedGames"))
// Set gesture margin from sides not to trigger system gesture navigation
gamesContainer.setGestureMargin(device.displayWidth / 5)
gamesContainer.fling(Direction.RIGHT)
device.waitForIdle()
gamesContainer.fling(Direction.LEFT)
}

fun MacrobenchmarkScope.waitForDataSources() {
device.wait(Until.hasObject(By.res("onboarding:datasources:content")), 10_000L)
}

fun MacrobenchmarkScope.moveToNextPage() {
device.findObject(By.res("onboarding:fab")).click()
device.waitForIdle()
}
}
13 changes: 13 additions & 0 deletions benchmark/src/main/java/com/mr3y/ludi/benchmark/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mr3y.ludi.benchmark

import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until

internal fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 1000L): UiObject2 {
if (wait(Until.hasObject(selector), timeout)) {
return findObject(selector)
}
error("Object with selector [$selector] not found")
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.kotlin.android).apply(false)
alias(libs.plugins.android.application).apply(false)
alias(libs.plugins.android.library).apply(false)
alias(libs.plugins.android.test).apply(false)
alias(libs.plugins.compose.multiplatform).apply(false)
alias(libs.plugins.spotless.plugin).apply(false)
alias(libs.plugins.ktlint.plugin).apply(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mr3y.ludi.gradle

import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.TestExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
Expand All @@ -25,6 +26,12 @@ class AndroidConventionPlugin : Plugin<Project> {
applyCommonAndroidConvention()
}
}
pluginManager.hasPlugin("com.android.test") -> {
val androidTestExtension = extensions.getByType<TestExtension>()
androidTestExtension.apply {
applyCommonAndroidConvention()
}
}
else -> {}
}
}
Expand Down
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ android-library = "com.android.library:8.1.2"
## ⬆ :8.3.0-alpha09"
## ⬆ :8.3.0-alpha10"
## ⬆ :8.3.0-alpha11"
android-test = "com.android.test:8.1.2"
baseline-profile = "androidx.baselineprofile:1.2.0"
kotlin-android = "org.jetbrains.kotlin.android:1.9.10"
kotlin-jvm = "org.jetbrains.kotlin.jvm:1.9.10"
kotlin-multiplatform = "org.jetbrains.kotlin.multiplatform:1.9.10"
Expand Down Expand Up @@ -229,6 +231,10 @@ junit = "junit:junit:4.13.2"
strikt = "io.strikt:strikt-core:0.34.1"
turbine = "app.cash.turbine:turbine:1.0.0"
robolectric = "org.robolectric:robolectric:4.10.3"
espresso-core = "androidx.test.espresso:espresso-core:3.5.1"
uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0"
profiler-installer = "androidx.profileinstaller:profileinstaller:1.3.1"
benchmark-macro-junit4 = "androidx.benchmark:benchmark-macro-junit4:1.2.0"
## ⬆ :4.11-beta-1"
## ⬆ :4.11-beta-2"
## ⬆ :4.11"
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ dependencyResolutionManagement {
rootProject.name = "Ludi"
include(":androidApp")
include(":shared")
include(":desktopApp")
include(":desktopApp")
include(":benchmark")
5 changes: 4 additions & 1 deletion shared/src/commonMain/kotlin/com/mr3y/ludi/shared/App.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mr3y.ludi.shared

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
Expand Down Expand Up @@ -30,7 +31,9 @@ fun App(
LocalWindowSizeClass provides calculateWindowSizeClass()
) {
if (showOnboardingScreen) {
Navigator(screen = OnboardingScreen)
Box(modifier = modifier.fillMaxSize()) {
Navigator(screen = OnboardingScreen)
}
} else {
HomeScreen(modifier = modifier.fillMaxSize())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -75,6 +76,7 @@ fun NewsSourcesPage(
.fillMaxWidth()
.semantics {
isTraversalGroup = true
testTag = "onboarding:datasources:content"
}
) {
allNewsDataSources.forEachIndexed { index, newsDataSource ->
Expand Down
Loading