Merge branch 'bw/initialMetrics' of github.com:android/nowinandroid into bw/initialMetrics

pull/145/head
Ben Weiss 3 years ago
commit e949749073
No known key found for this signature in database
GPG Key ID: 8424F9C1E763A74C

@ -22,12 +22,21 @@ import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.core.view.WindowCompat
import androidx.metrics.performance.JankStats
import com.google.samples.apps.nowinandroid.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var lazyStats: dagger.Lazy<JankStats>
@Inject
lateinit var jankFrameListener: JankStats.OnFrameListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -35,6 +44,22 @@ class MainActivity : ComponentActivity() {
// including IME animations
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { NiaApp(calculateWindowSizeClass(this)) }
setContent {
NiaApp(calculateWindowSizeClass(this))
}
}
override fun onResume() {
super.onResume()
isTrackingEnabled(true)
}
override fun onPause() {
super.onPause()
isTrackingEnabled(false)
}
private fun isTrackingEnabled(enabled: Boolean) {
lazyStats.get().isTrackingEnabled = enabled
}
}

@ -0,0 +1,63 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.di
import android.app.Activity
import android.util.Log
import android.view.Window
import androidx.metrics.performance.JankStats
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
@Module
@InstallIn(ActivityComponent::class)
object JankStatsModule {
@Provides
fun providesOnFrameListener(): JankStats.OnFrameListener {
return JankStats.OnFrameListener { frameData ->
// Make sure to only log janky frames.
if (frameData.isJank) {
// We're currently logging this but would better report it to a backend.
Log.v("NiA Jank", frameData.toString())
}
}
}
@Provides
fun providesWindow(activity: Activity): Window {
return activity.window
}
@Provides
fun providesDefaultExecutor(): Executor {
return Dispatchers.Default.asExecutor()
}
@Provides
fun providesJankStats(
window: Window,
executor: Executor,
frameListener: JankStats.OnFrameListener
): JankStats {
return JankStats.createAndTrack(window, executor, frameListener)
}
}

@ -24,6 +24,7 @@ import androidx.compose.material.icons.outlined.Upcoming
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.feature.foryou.R.string.for_you
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouDestination
import com.google.samples.apps.nowinandroid.feature.interests.R.string.interests
@ -41,6 +42,7 @@ import com.google.samples.apps.nowinandroid.feature.interests.navigation.Interes
class NiaTopLevelNavigation(private val navController: NavHostController) {
fun navigateTo(destination: TopLevelDestination) {
trace("Navigation: $destination") {
navController.navigate(destination.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
@ -55,6 +57,7 @@ class NiaTopLevelNavigation(private val navController: NavHostController) {
restoreState = true
}
}
}
}
data class TopLevelDestination(

@ -46,10 +46,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect
import com.google.samples.apps.nowinandroid.core.ui.component.NiaBackground
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
@ -62,6 +64,18 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
fun NiaApp(windowSizeClass: WindowSizeClass) {
NiaTheme {
val navController = rememberNavController()
JankMetricDisposableEffect(navController) { metricsHolder ->
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
metricsHolder.state?.addState("Navigation", destination.route.toString())
}
navController.addOnDestinationChangedListener(listener)
onDispose {
navController.removeOnDestinationChangedListener(listener)
}
}
val niaTopLevelNavigation = remember(navController) {
NiaTopLevelNavigation(navController)
}

@ -28,6 +28,7 @@ dependencies {
api(libs.androidx.hilt.navigation.compose)
api(libs.androidx.navigation.compose)
implementation(libs.androidx.tracing.ktx)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}

@ -43,4 +43,6 @@ dependencies {
api(libs.androidx.compose.ui.util)
api(libs.androidx.compose.runtime)
api(libs.androidx.compose.runtime.livedata)
api(libs.androidx.metrics)
api(libs.androidx.tracing.ktx)
}

@ -0,0 +1,73 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import androidx.metrics.performance.PerformanceMetricsState
import androidx.metrics.performance.PerformanceMetricsState.MetricsStateHolder
import kotlinx.coroutines.CoroutineScope
/**
* Retrieves [PerformanceMetricsState.MetricsStateHolder] from current [LocalView] and
* remembers it until the View changes.
* @see PerformanceMetricsState.getForHierarchy
*/
@Composable
fun rememberMetricsStateHolder(): MetricsStateHolder {
val localView = LocalView.current
return remember(localView) {
PerformanceMetricsState.getForHierarchy(localView)
}
}
/**
* Convenience function to work with [PerformanceMetricsState] state. The side effect is
* re-launched if any of the [keys] value is not equal to the previous composition.
* @see JankMetricDisposableEffect if you need to work with DisposableEffect to cleanup added state.
*/
@Composable
fun JankMetricEffect(
vararg keys: Any?,
reportMetric: suspend CoroutineScope.(state: MetricsStateHolder) -> Unit
) {
val metrics = rememberMetricsStateHolder()
LaunchedEffect(metrics, *keys) {
reportMetric(metrics)
}
}
/**
* Convenience function to work with [PerformanceMetricsState] state that needs to be cleaned up.
* The side effect is re-launched if any of the [keys] value is not equal to the previous composition.
*/
@Composable
fun JankMetricDisposableEffect(
vararg keys: Any?,
reportMetric: DisposableEffectScope.(state: MetricsStateHolder) -> DisposableEffectResult
) {
val metrics = rememberMetricsStateHolder()
DisposableEffect(metrics, *keys) {
reportMetric(this, metrics)
}
}

@ -18,10 +18,8 @@ package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -30,6 +28,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@ -41,6 +40,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -57,6 +57,9 @@ import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.ui.FollowButton
import com.google.samples.apps.nowinandroid.core.ui.JankMetricEffect
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
@Composable
fun AuthorsCarousel(
@ -64,11 +67,25 @@ fun AuthorsCarousel(
onAuthorClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyRow(
modifier = modifier,
contentPadding = PaddingValues(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
val lazyListState = rememberLazyListState()
JankMetricEffect(lazyListState) { metricsHolder ->
combine(
snapshotFlow { lazyListState.isScrollInProgress },
snapshotFlow { lazyListState.firstVisibleItemIndex }
) { isScrollInProgress, firstVisibleItemIndex ->
if (isScrollInProgress) {
metricsHolder.state?.addState(
"ForYou:AuthorsCarousel:Scrolling",
"Index=$firstVisibleItemIndex"
)
} else {
metricsHolder.state?.removeState("ForYou:AuthorsCarousel:Scrolling")
}
}.collect()
}
LazyRow(modifier, lazyListState) {
items(items = authors, key = { item -> item.author.id }) { followableAuthor ->
AuthorItem(
author = followableAuthor.author,

@ -44,7 +44,9 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@ -63,6 +65,7 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -86,6 +89,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.JankMetricEffect
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel
import com.google.samples.apps.nowinandroid.core.ui.NewsResourceCardExpanded
import com.google.samples.apps.nowinandroid.core.ui.component.NiaFilledButton
@ -96,6 +100,8 @@ import com.google.samples.apps.nowinandroid.core.ui.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTypography
import kotlin.math.floor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.datetime.Instant
@Composable
@ -166,7 +172,25 @@ fun ForYouScreen(
else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1)
}
val lazyListState = rememberLazyListState()
JankMetricEffect(lazyListState) { metricsHolder ->
combine(
snapshotFlow { lazyListState.isScrollInProgress },
snapshotFlow { lazyListState.firstVisibleItemIndex },
) { isScrollInProgress, firstVisibleItemIndex ->
if (isScrollInProgress) {
metricsHolder.state?.addState(
"ForYou:Feed:Scrolling",
"index=$firstVisibleItemIndex"
)
} else {
metricsHolder.state?.removeState("ForYou:Feed:Scrolling")
}
}.collect()
}
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize(),
) {
InterestsSelection(
@ -297,7 +321,25 @@ private fun TopicSelection(
onTopicCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val lazyGridState = rememberLazyGridState()
JankMetricEffect(lazyGridState) { metricsHolder ->
combine(
snapshotFlow { lazyGridState.isScrollInProgress },
snapshotFlow { lazyGridState.firstVisibleItemIndex },
) { isScrollInProgress, firstVisibleItemIndex ->
if (isScrollInProgress) {
metricsHolder.state?.addState(
"ForYou:TopicSelection:Scrolling",
"index=$firstVisibleItemIndex"
)
} else {
metricsHolder.state?.removeState("ForYou:TopicSelection:Scrolling")
}
}.collect()
}
LazyHorizontalGrid(
state = lazyGridState,
rows = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),

@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.samples.apps.nowinandroid.core.ui.JankMetricDisposableEffect
import com.google.samples.apps.nowinandroid.core.ui.LoadingWheel
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTab
import com.google.samples.apps.nowinandroid.core.ui.component.NiaTabRow
@ -50,7 +51,6 @@ fun InterestsRoute(
) {
val uiState by viewModel.uiState.collectAsState()
val tabState by viewModel.tabState.collectAsState()
InterestsScreen(
uiState = uiState,
tabState = tabState,
@ -61,6 +61,14 @@ fun InterestsRoute(
switchTab = viewModel::switchTab,
modifier = modifier
)
JankMetricDisposableEffect(tabState) { metricsHolder ->
metricsHolder.state?.addState("Interests:TabState", "currentIndex:${tabState.currentIndex}")
onDispose {
metricsHolder.state?.removeState("Interests:TabState")
}
}
}
@Composable

@ -14,12 +14,14 @@ androidxHiltNavigationCompose = "1.0.0"
androidxLifecycle = "2.5.0-rc01"
androidxMacroBenchmark = "1.1.0-rc03"
androidxNavigation = "2.4.2"
androidxMetrics = "1.0.0-alpha01"
androidxProfileinstaller = "1.2.0-beta01"
androidxSavedState = "1.1.0"
androidxStartup = "1.1.1"
androidxWindowManager = "1.0.0"
androidxTest = "1.4.0"
androidxTestExt = "1.1.3"
androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0"
androidxWork = "2.7.1"
coil = "2.0.0-rc01"
@ -69,6 +71,7 @@ androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", ve
androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref= "androidxSavedState"}
@ -80,6 +83,7 @@ androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espres
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" }
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
androidx-tracing-ktx = {group = "androidx.tracing", name="tracing-ktx", version.ref = "androidxTracing" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil"}

@ -35,6 +35,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.startup)
implementation(libs.androidx.work.ktx)
implementation(libs.hilt.ext.work)

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.sync.workers
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.tracing.traceAsync
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
@ -59,6 +60,7 @@ class SyncWorker @AssistedInject constructor(
appContext.syncForegroundInfo()
override suspend fun doWork(): Result = withContext(ioDispatcher) {
traceAsync("Sync", 0) {
// First sync the repositories in parallel
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
@ -69,6 +71,7 @@ class SyncWorker @AssistedInject constructor(
if (syncedSuccessfully) Result.success()
else Result.retry()
}
}
override suspend fun getChangeListVersions(): ChangeListVersions =
niaPreferences.getChangeListVersions()

Loading…
Cancel
Save