Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Formula Android] Add event listener to track performance. #313

Merged
merged 1 commit into from
Nov 30, 2023
Merged
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
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].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be called before the DI injection call in [Application.onCreate]? If yes - might be worth mentioning it here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary because usually, the logic will not run until a fragment is shown.

*
* @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
Loading