Skip to content

Commit

Permalink
[Formula Android] Add event listener to track performance.
Browse files Browse the repository at this point in the history
  • Loading branch information
Laimiux committed Nov 30, 2023
1 parent d9b5c98 commit 5677405
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,7 +20,13 @@ abstract class ComposeViewFactory<RenderModel> : ViewFactory<RenderModel> {
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
Expand Down
6 changes: 6 additions & 0 deletions formula-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ 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?
if (appManager != null) {
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)
Expand All @@ -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]
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class FeatureView<RenderModel>(
val lifecycleCallbacks: FragmentLifecycleCallback? = null,
) {
class State<RenderModel>(
val fragmentId: FragmentId,
val environment: FragmentEnvironment,
val observable: Observable<RenderModel>,
val onError: (Throwable) -> Unit,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any> {
Expand Down Expand Up @@ -49,10 +50,9 @@ class FormulaFragment : Fragment(), BaseFormulaFragment<Any> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -33,8 +34,14 @@ internal class FeatureBinding<in Component, in Dependencies, in Key : FragmentKe
Action.onData(fragmentId).onEvent {
transition {
try {
val start = SystemClock.uptimeMillis()
val dependencies = toDependencies(input.component)
val feature = feature.initialize(dependencies, key as Key)
val end = SystemClock.uptimeMillis()
input.environment.eventListener?.onFeatureInitialized(
fragmentId = fragmentId,
durationInMillis = end - start,
)
input.onInitializeFeature(FeatureEvent.Init(fragmentId, feature))
} catch (e: Exception) {
input.onInitializeFeature(FeatureEvent.Failure(fragmentId, e))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ internal object FormulaFragmentDelegate {
}


fun logFragmentError(key: FragmentKey, error: Throwable) {
fragmentEnvironment().onScreenError(key, error)
}

private fun fragmentEnvironment(): FragmentEnvironment {
fun fragmentEnvironment(): FragmentEnvironment {
return checkNotNull(fragmentEnvironment) { "FormulaAndroid.init() not called." }
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.instacart.formula.android.internal

import android.os.SystemClock
import android.view.LayoutInflater
import android.view.ViewGroup
import com.instacart.formula.android.FeatureView
import com.instacart.formula.android.ViewFactory
import com.instacart.formula.android.FeatureEvent
import com.instacart.formula.android.FragmentEnvironment
import com.instacart.formula.android.FragmentId
import java.lang.IllegalStateException

internal class FormulaFragmentViewFactory(
private val environment: FragmentEnvironment,
private val fragmentId: FragmentId,
private val featureProvider: FeatureProvider,
) : ViewFactory<Any> {
Expand All @@ -17,6 +20,7 @@ internal class FormulaFragmentViewFactory(

@Suppress("UNCHECKED_CAST")
override fun create(inflater: LayoutInflater, container: ViewGroup?): FeatureView<Any> {
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) {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ internal class FragmentFlowRenderView(

fun viewFactory(fragment: FormulaFragment): ViewFactory<Any> {
initializeFragmentInstanceIdIfNeeded(fragment)
return FormulaFragmentViewFactory(fragment.getFormulaFragmentId(), featureProvider)
return FormulaFragmentViewFactory(
environment = fragmentEnvironment,
fragmentId = fragment.getFormulaFragmentId(),
featureProvider = featureProvider,
)
}

private fun notifyLifecycleStateChanged(fragment: Fragment, newState: Lifecycle.State) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,11 +13,18 @@ internal class FeatureViewBindFunction<RenderModel>(
private val render: Renderer<RenderModel>
) : (FeatureView.State<RenderModel>) -> Cancelable? {
override fun invoke(state: FeatureView.State<RenderModel>): 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 {
Expand Down

0 comments on commit 5677405

Please sign in to comment.