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

Interaction latency rules #38

Merged
merged 8 commits into from
Sep 29, 2022
Merged

Interaction latency rules #38

merged 8 commits into from
Sep 29, 2022

Conversation

pyricau
Copy link
Member

@pyricau pyricau commented Sep 29, 2022

Redesigning the API and implementation for tracking interaction latency, with a primary goal of decoupling the code where we send signals (events) from the place where we tie those events together an interaction.

Example usage code

interface PosInteractionEvent
interface PosInteraction

object OpenCheckoutApplet : PosInteraction

class Navigation(val destination: Any) : PosInteractionEvent

class CheckoutApplet : PosInteraction

object TapItem : PosInteractionEvent {
  val itemId = ""
}

object ItemVisibleInCart : PosInteractionEvent {
  val itemId = ""
}

class AddItemToCart(val itemId: String) : PosInteraction

object KeypadVisible : PosInteractionEvent
object UserPressedBack : PosInteractionEvent

fun setup() {

  val client = InteractionRuleClient<PosInteractionEvent, PosInteraction>()

  client.sendEvent(KeypadVisible)
  client.sendEvent(Navigation(Any()))

  val configuredRule = client.addInteractionRule<OpenCheckoutApplet> {
    onEvent<Navigation> { navigation ->
      if (navigation.destination is CheckoutApplet) {
        cancelRunningInteractions()
        startInteraction(OpenCheckoutApplet, onCancel = {
          Log.d("event canceled", it.cancelReason)
        })
      }
    }
    onEvent<UserPressedBack> {
      runningInteractions().singleOrNull()?.cancel("user pressed back")
    }
    onEvent<KeypadVisible> {
      runningInteractions().singleOrNull()?.finishOnFrameRendered { result ->
        logToAnalytics(result)
      }
    }
  }

  client.addInteractionRule<AddItemToCart> {
    onEvent<TapItem> { tapItem ->
      startInteraction(AddItemToCart(tapItem.itemId))
    }
    onEvent<ItemVisibleInCart> { itemVisibleInCart ->
      val addItemToCart = runningInteractions().firstOrNull {
        it.interaction.itemId == itemVisibleInCart.itemId
      }
      addItemToCart?.let { interaction ->
        interaction.finishOnFrameRendered { result ->
          logToAnalytics(result)
        }
      }
    }
  }

  // when we don't need this rule anymore
  configuredRule.remove()
}

@@ -36,5 +39,35 @@ interface InputTracker {

class DeliveredInput<T : InputEvent>(
val event: T,
val deliveryUptimeMillis: Long
)
val deliveryUptime: Duration,
Copy link
Member Author

Choose a reason for hiding this comment

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

All these years I had no idea we had a Duration class in Kotlin (and also another one in Java!!)

Comment on lines +7 to +9
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit.MILLISECONDS
Copy link

Choose a reason for hiding this comment

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

@0legg Should PY be using 310 alternatives here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm really taking a liking to Kotlin's duration class, looks nicer than the JDK ones.

val stateHolder = PerformanceMetricsState.getForHierarchy(textView).state!!
stateHolder.addState("textview", "updated at $date")
interactionEventReceiver.sendEvent(OnMainActivityButtonClick(element, previousText, newText))
// val stateHolder = PerformanceMetricsState.getForHierarchy(textView).state!!

Choose a reason for hiding this comment

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

Nit: Remove commented code?


override val currentKeyEvent: DeliveredInput<KeyEvent>?
get() = currentKeyEventLocal.get()

private val motionEventTriggeringClickLocal = ThreadLocal<DeliveredInput<MotionEvent>>()
/**
* The thread locals here aren't in charge of actually holding the event holder for the duration

Choose a reason for hiding this comment

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

Nit: This would be easier to read if it were broken up into multiple sentences.

* The thread locals here aren't in charge of actually holding the event holder for the duration
* of its lifecycle, but instead of holding the event in specific time spans (wrapping onClick
* callback invocations) so that we can reach back here from an onClick and capture the event but
* also ensure that if we reach back outside of an onClick sandwich we actually find nothing

Choose a reason for hiding this comment

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

Nit: find nothing event if overall -> Seems like it's missing a word or two in there?

// if this event is consumed as part of the frame we did the increment in.
// There's a slight edge case: if the event consumption triggered in between doFrame and
// the post at front of queue, the count would be short by 1. We can live with this, it's
// unlikely to happen unless and even is triggered from a postAtFront.

Choose a reason for hiding this comment

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

Nit: and even -> an event?

// holder. Why replace it? Because we want to increase the frame count over time, but we
// want to do that by swapping an immutable event, so that if we capture such event at
// time N and then the count gets updated at N + 1, the count update isn't reflected in
// the code that captured the event a time N.

Choose a reason for hiding this comment

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

Nit: a time -> at time

val event: InputEventType,
val deliveryUptime: Duration,
val framesSinceDelivery: Int,
private var endTrace: ((() -> Unit))?

Choose a reason for hiding this comment

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

Nit: Is there an unnecessary set of parentheses there?

) {

val eventUptime: Duration
get() = event.eventTime.milliseconds

Choose a reason for hiding this comment

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

Nit: Does this need to be a custom getter, or can it just be initialized? Can event.eventTime.milliseconds change?

Copy link
Member Author

Choose a reason for hiding this comment

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

It won't change, but as a general practice I avoid creating backing fields when delegation works just fine and is cheap, especially if the backing field might never be actually read.

private val onCancel: (CanceledInteractionResult<InteractionType>) -> Unit
) : RunningInteraction<InteractionType>, FinishingInteraction<InteractionType>, FrameCallback {

private var frameCount: Int = if (isChoreographerDoingFrame()) {

Choose a reason for hiding this comment

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

Nit: One line without braces?

override fun sendEvent(event: EventType) {
val eventSentUptime = System.nanoTime().nanoseconds
if (isMainThread) {
for (engine in interactionEngines) {

Choose a reason for hiding this comment

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

Nit: interactionEngines.forEach?

Copy link
Member Author

Choose a reason for hiding this comment

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

I find traditional for loops a bit more readable but to be honest it's not like a strongly informed opinion.

} else {
mainHandler.post {
for (engine in interactionEngines) {
engine.sendEvent(event, eventSentUptime, null)

Choose a reason for hiding this comment

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

Why is the interactionInput parameter null?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants