From 66bf74dc0df58ace1ffcec43d039bf72e8d1a39d 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 ++++++ .../com/instacart/formula/FormulaAndroid.kt | 20 ++++++++++++--- .../instacart/formula/android/FeatureView.kt | 3 ++- .../formula/android/FormulaFragment.kt | 6 ++--- .../formula/android/FragmentEnvironment.kt | 25 ++++++++++++++++++- .../android/internal/FeatureBinding.kt | 8 ++++++ .../internal/FormulaFragmentDelegate.kt | 2 +- .../internal/FormulaFragmentViewFactory.kt | 9 ++++++- .../internal/FragmentFlowRenderView.kt | 6 ++++- .../android/views/FeatureViewBindFunction.kt | 10 +++++++- 10 files changed, 84 insertions(+), 12 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/src/main/java/com/instacart/formula/FormulaAndroid.kt b/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt index 189e8e4d4..9158a7174 100644 --- a/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt +++ b/formula-android/src/main/java/com/instacart/formula/FormulaAndroid.kt @@ -27,8 +27,7 @@ object FormulaAndroid { */ 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 +35,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 +45,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..f9800d6ae 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,15 @@ 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 {