From 567740502d9372ef80ee8ebc44baf124649f6e36 Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Thu, 30 Nov 2023 11:36:46 -0800 Subject: [PATCH] [Formula Android] Add event listener to track performance. --- .../android/compose/ComposeViewFactory.kt | 7 ++++++ formula-android/build.gradle | 6 +++++ .../com/instacart/formula/FormulaAndroid.kt | 23 +++++++++++++---- .../instacart/formula/android/FeatureView.kt | 3 ++- .../formula/android/FormulaFragment.kt | 6 ++--- .../formula/android/FragmentEnvironment.kt | 25 ++++++++++++++++++- .../android/internal/FeatureBinding.kt | 7 ++++++ .../internal/FormulaFragmentDelegate.kt | 6 +---- .../internal/FormulaFragmentViewFactory.kt | 9 ++++++- .../internal/FragmentFlowRenderView.kt | 6 ++++- .../android/views/FeatureViewBindFunction.kt | 10 +++++++- 11 files changed, 90 insertions(+), 18 deletions(-) diff --git a/formula-android-compose/src/main/java/com/instacart/formula/android/compose/ComposeViewFactory.kt b/formula-android-compose/src/main/java/com/instacart/formula/android/compose/ComposeViewFactory.kt index 973115c2b..f0c1bcd16 100644 --- a/formula-android-compose/src/main/java/com/instacart/formula/android/compose/ComposeViewFactory.kt +++ b/formula-android-compose/src/main/java/com/instacart/formula/android/compose/ComposeViewFactory.kt @@ -1,5 +1,6 @@ package com.instacart.formula.android.compose +import android.os.SystemClock import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.runtime.Composable @@ -19,7 +20,13 @@ abstract class ComposeViewFactory : ViewFactory { view.setContent { val model = it.observable.subscribeAsState(null).value if (model != null) { + val start = SystemClock.uptimeMillis() Content(model) + val end = SystemClock.uptimeMillis() + it.environment.eventListener?.onRendered( + fragmentId = it.fragmentId, + durationInMillis = end - start, + ) } } null diff --git a/formula-android/build.gradle b/formula-android/build.gradle index 58444e952..d5f886bf7 100644 --- a/formula-android/build.gradle +++ b/formula-android/build.gradle @@ -18,6 +18,12 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + testOptions { + unitTests { + returnDefaultValues true + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { diff --git a/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt b/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt index 189e8e4d4..d7949053f 100644 --- a/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt +++ b/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt @@ -22,13 +22,11 @@ object FormulaAndroid { /** * Initializes Formula Android integration. Should be called within [Application.onCreate]. * - * @param logger A logger for debug Formula Android events. - * @param onFragmentError A global handler for fragment errors. Override this to log the crashes. + * @param fragmentEnvironment Environment model that configures various event listeners. */ fun init( application: Application, - logger: ((String) -> Unit)? = null, - onFragmentError: (FragmentKey, Throwable) -> Unit = { _, it -> throw it }, + fragmentEnvironment: FragmentEnvironment = FragmentEnvironment(), activities: ActivityConfigurator.() -> Unit ) { // Should we allow re-initialization? @@ -36,7 +34,6 @@ object FormulaAndroid { throw IllegalStateException("can only initialize the store once.") } - val fragmentEnvironment = FragmentEnvironment(logger ?: {}, onFragmentError) val factory = ActivityStoreFactory(fragmentEnvironment, activities) val appManager = AppManager(factory) application.registerActivityLifecycleCallbacks(appManager) @@ -47,6 +44,22 @@ object FormulaAndroid { FormulaFragmentDelegate.fragmentEnvironment = fragmentEnvironment } + /** + * Initializes Formula Android integration. Should be called within [Application.onCreate]. + * + * @param logger A logger for debug Formula Android events. + * @param onFragmentError A global handler for fragment errors. Override this to log the crashes. + */ + fun init( + application: Application, + logger: ((String) -> Unit)? = null, + onFragmentError: (FragmentKey, Throwable) -> Unit = { _, it -> throw it }, + activities: ActivityConfigurator.() -> Unit + ) { + val fragmentEnvironment = FragmentEnvironment(logger ?: {}, onFragmentError) + init(application, fragmentEnvironment, activities) + } + /** * Call this method in [FragmentActivity.onCreate] before calling [FragmentActivity.super.onCreate] */ diff --git a/formula-android/src/main/java/com/instacart/formula/android/FeatureView.kt b/formula-android/src/main/java/com/instacart/formula/android/FeatureView.kt index e491b2a13..b2ced889e 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/FeatureView.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/FeatureView.kt @@ -22,7 +22,8 @@ class FeatureView( val lifecycleCallbacks: FragmentLifecycleCallback? = null, ) { class State( + val fragmentId: FragmentId, + val environment: FragmentEnvironment, val observable: Observable, - val onError: (Throwable) -> Unit, ) } \ No newline at end of file diff --git a/formula-android/src/main/java/com/instacart/formula/android/FormulaFragment.kt b/formula-android/src/main/java/com/instacart/formula/android/FormulaFragment.kt index 92db9c432..0c0e421e8 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/FormulaFragment.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/FormulaFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import com.instacart.formula.Cancelable import com.instacart.formula.android.internal.FormulaFragmentDelegate +import com.instacart.formula.android.internal.getFormulaFragmentId import com.jakewharton.rxrelay3.BehaviorRelay class FormulaFragment : Fragment(), BaseFormulaFragment { @@ -49,10 +50,9 @@ class FormulaFragment : Fragment(), BaseFormulaFragment { super.onViewCreated(view, savedInstanceState) featureView?.let { value -> val state = FeatureView.State( + fragmentId = getFormulaFragmentId(), + environment = FormulaFragmentDelegate.fragmentEnvironment(), observable = stateRelay, - onError = { - FormulaFragmentDelegate.logFragmentError(key, it) - } ) cancelable = value.bind(state) this.lifecycleCallback = value.lifecycleCallbacks diff --git a/formula-android/src/main/java/com/instacart/formula/android/FragmentEnvironment.kt b/formula-android/src/main/java/com/instacart/formula/android/FragmentEnvironment.kt index aa4cff03d..f907b611b 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/FragmentEnvironment.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/FragmentEnvironment.kt @@ -3,4 +3,27 @@ package com.instacart.formula.android data class FragmentEnvironment( val logger: (String) -> Unit = {}, val onScreenError: (FragmentKey, Throwable) -> Unit = { _, it -> throw it }, -) + val eventListener: EventListener? = null, +) { + + /** + * Introspection API to track various formula fragment events and their performance. + */ + interface EventListener { + + /** + * Called after [FeatureFactory.initialize] is called. + */ + fun onFeatureInitialized(fragmentId: FragmentId, durationInMillis: Long) + + /** + * Called when [FormulaFragment] view is inflated. + */ + fun onViewInflated(fragmentId: FragmentId, durationInMillis: Long) + + /** + * Called after render model was applied to the [FeatureView]. + */ + fun onRendered(fragmentId: FragmentId, durationInMillis: Long) + } +} diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureBinding.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureBinding.kt index a38b2313e..5710e18bd 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureBinding.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/internal/FeatureBinding.kt @@ -1,5 +1,6 @@ package com.instacart.formula.android.internal +import android.os.SystemClock import com.instacart.formula.Action import com.instacart.formula.Evaluation import com.instacart.formula.Formula @@ -33,8 +34,14 @@ internal class FeatureBinding { @@ -17,6 +20,7 @@ internal class FormulaFragmentViewFactory( @Suppress("UNCHECKED_CAST") override fun create(inflater: LayoutInflater, container: ViewGroup?): FeatureView { + val start = SystemClock.uptimeMillis() val key = fragmentId.key val featureEvent = featureProvider.getFeature(fragmentId) ?: throw IllegalStateException("Could not find feature for $key.") val viewFactory = factory ?: when (featureEvent) { @@ -31,6 +35,9 @@ internal class FormulaFragmentViewFactory( } } this.factory = viewFactory - return viewFactory.create(inflater, container) + val view = viewFactory.create(inflater, container) + val endTime = SystemClock.uptimeMillis() + environment.eventListener?.onViewInflated(fragmentId, endTime - start) + return view } } \ No newline at end of file diff --git a/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentFlowRenderView.kt b/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentFlowRenderView.kt index 30fe8eee6..1a29f17b6 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentFlowRenderView.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/internal/FragmentFlowRenderView.kt @@ -142,7 +142,11 @@ internal class FragmentFlowRenderView( fun viewFactory(fragment: FormulaFragment): ViewFactory { initializeFragmentInstanceIdIfNeeded(fragment) - return FormulaFragmentViewFactory(fragment.getFormulaFragmentId(), featureProvider) + return FormulaFragmentViewFactory( + environment = fragmentEnvironment, + fragmentId = fragment.getFormulaFragmentId(), + featureProvider = featureProvider, + ) } private fun notifyLifecycleStateChanged(fragment: Fragment, newState: Lifecycle.State) { diff --git a/formula-android/src/main/java/com/instacart/formula/android/views/FeatureViewBindFunction.kt b/formula-android/src/main/java/com/instacart/formula/android/views/FeatureViewBindFunction.kt index c452ee173..4b29a51c5 100644 --- a/formula-android/src/main/java/com/instacart/formula/android/views/FeatureViewBindFunction.kt +++ b/formula-android/src/main/java/com/instacart/formula/android/views/FeatureViewBindFunction.kt @@ -1,5 +1,6 @@ package com.instacart.formula.android.views +import android.os.SystemClock import com.instacart.formula.Cancelable import com.instacart.formula.Renderer import com.instacart.formula.android.FeatureView @@ -12,11 +13,18 @@ internal class FeatureViewBindFunction( private val render: Renderer ) : (FeatureView.State) -> Cancelable? { override fun invoke(state: FeatureView.State): Cancelable { + val environment = state.environment val disposable = state.observable.subscribe { try { + val start = SystemClock.uptimeMillis() render(it) + val end = SystemClock.uptimeMillis() + environment.eventListener?.onRendered( + fragmentId = state.fragmentId, + durationInMillis = end - start, + ) } catch (exception: Exception) { - state.onError(exception) + environment.onScreenError(state.fragmentId.key, exception) } } return Cancelable {