Skip to content

Commit

Permalink
Add ability to integrate a Events listener into a ComponentTree
Browse files Browse the repository at this point in the history
Summary:
As part of the process of replacing `ComponentsLogger` with an API that relies on the new Events API, I'm adding the ability to listen to events associated with a ComponentTree. These events can be ComponentTree/RenderCore related and they will use the `renderStateId/componentTreeId` as the reference.

## Client Side

The client will only define a `ComponentTreeDebugEventListener` where it specifies the action to perform on an `DebugEvent` and the set of events it is interested in.

## Internals

Whenever a listener is present, we will wrap it inside a `DebugEventSubscriber` which will only propagate the events related to that ComponentTree and for which the listener is interested in. This subscriber will be automatically subscriber and will unsubscribe during `release`.

Reviewed By: adityasharat

Differential Revision: D46080849

fbshipit-source-id: 2c2ade11b89feede22e037615aca915a3d3a1288
  • Loading branch information
Fabio Carballo authored and facebook-github-bot committed May 25, 2023
1 parent e204991 commit d71cdbe
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 24 deletions.
19 changes: 0 additions & 19 deletions litho-core/src/main/java/com/facebook/litho/ComponentContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,25 +176,6 @@ public ComponentContext(ComponentContext context, @Nullable TreeProps treeProps)
mLithoConfiguration = context.mLithoConfiguration;
}

private static LithoConfiguration mergeConfigurationWithNewLogTagAndLogger(
LithoConfiguration lithoConfiguration,
@Nullable String logTag,
@Nullable ComponentsLogger logger) {
return new LithoConfiguration(
lithoConfiguration.mComponentsConfiguration,
lithoConfiguration.areTransitionsEnabled,
lithoConfiguration.isReconciliationEnabled,
lithoConfiguration.isVisibilityProcessingEnabled,
lithoConfiguration.isNullNodeEnabled,
lithoConfiguration.mountContentPreallocationHandler,
lithoConfiguration.incrementalMountEnabled,
lithoConfiguration.errorEventHandler,
logTag != null ? logTag : lithoConfiguration.logTag,
logger != null ? logger : lithoConfiguration.logger,
lithoConfiguration.renderUnitIdGenerator,
lithoConfiguration.visibilityBoundsTransformer);
}

ComponentContext makeNewCopy() {
return new ComponentContext(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ private static ComponentTree.LithoConfiguration mergeConfigurationWithNewLogTagA
logTag != null ? logTag : lithoConfiguration.logTag,
logger != null ? logger : lithoConfiguration.logger,
lithoConfiguration.renderUnitIdGenerator,
lithoConfiguration.visibilityBoundsTransformer);
lithoConfiguration.visibilityBoundsTransformer,
lithoConfiguration.debugEventListener);
}

public static ComponentTree.LithoConfiguration buildDefaultLithoConfiguration(
Expand Down Expand Up @@ -121,6 +122,7 @@ public static ComponentTree.LithoConfiguration buildDefaultLithoConfiguration(
treeID != ComponentTree.INVALID_ID
? new RenderUnitIdGenerator(treeID)
: null, // TODO check if we can make this not nullable and always instantiate one
transformer);
transformer,
null);
}
}
35 changes: 33 additions & 2 deletions litho-core/src/main/java/com/facebook/litho/ComponentTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public class ComponentTree
private boolean mInAttach = false;

@Nullable private final ComponentTreeTimeMachine mTimeMachine;
@Nullable private final ComponentTreeDebugEventsSubscriber mDebugEventsSubscriber;

@Override
public void onMovedToState(LithoLifecycle state) {
Expand Down Expand Up @@ -471,7 +472,9 @@ protected ComponentTree(Builder builder) {
logTag,
logger,
renderUnitIdGenerator,
builder.visibilityBoundsTransformer);
builder.visibilityBoundsTransformer,
builder.componentTreeDebugEventListener);

mContext = ComponentContextUtils.withComponentTree(builder.context, config, this);

if (builder.mLifecycleProvider != null) {
Expand All @@ -484,6 +487,20 @@ protected ComponentTree(Builder builder) {
mTimeMachine = null;
}

if (config.debugEventListener != null) {
mDebugEventsSubscriber =
new ComponentTreeDebugEventsSubscriber(
mId,
config.debugEventListener.getEvents(),
debugEvent -> {
config.debugEventListener.onEvent(debugEvent);
return Unit.INSTANCE;
});
DebugEventBus.subscribe(mDebugEventsSubscriber);
} else {
mDebugEventsSubscriber = null;
}

if (ComponentsConfiguration.enableStateUpdatesBatching) {
mBatchedStateUpdatesStrategy = new PostStateUpdateToChoreographerCallback();
} else {
Expand Down Expand Up @@ -2701,6 +2718,10 @@ public void release() {
}

synchronized (this) {
if (mDebugEventsSubscriber != null) {
DebugEventBus.unsubscribe(mDebugEventsSubscriber);
}

if (mBatchedStateUpdatesStrategy != null) {
mBatchedStateUpdatesStrategy.release();
}
Expand Down Expand Up @@ -3107,6 +3128,7 @@ public static final class LithoConfiguration {
@Nullable final ComponentsLogger logger;
final RenderUnitIdGenerator renderUnitIdGenerator;
@Nullable final VisibilityBoundsTransformer visibilityBoundsTransformer;
@Nullable final ComponentTreeDebugEventListener debugEventListener;

public LithoConfiguration(
final ComponentsConfiguration config,
Expand All @@ -3120,7 +3142,8 @@ public LithoConfiguration(
String logTag,
@Nullable ComponentsLogger logger,
RenderUnitIdGenerator renderUnitIdGenerator,
@Nullable VisibilityBoundsTransformer visibilityBoundsTransformer) {
@Nullable VisibilityBoundsTransformer visibilityBoundsTransformer,
@Nullable ComponentTreeDebugEventListener debugEventListener) {
this.mComponentsConfiguration = config;
this.areTransitionsEnabled = areTransitionsEnabled;
this.isReconciliationEnabled = isReconciliationEnabled;
Expand All @@ -3134,6 +3157,7 @@ public LithoConfiguration(
this.logger = logger;
this.renderUnitIdGenerator = renderUnitIdGenerator;
this.visibilityBoundsTransformer = visibilityBoundsTransformer;
this.debugEventListener = debugEventListener;
}
}

Expand Down Expand Up @@ -3166,6 +3190,7 @@ public static class Builder {

private @Nullable RenderUnitIdGenerator mRenderUnitIdGenerator;
private @Nullable VisibilityBoundsTransformer visibilityBoundsTransformer;
private @Nullable ComponentTreeDebugEventListener componentTreeDebugEventListener;

protected Builder(ComponentContext context) {
this.context = context;
Expand Down Expand Up @@ -3196,6 +3221,12 @@ public Builder withLithoLifecycleProvider(LithoLifecycleProvider lifecycleProvid
return this;
}

public Builder withComponentTreeDebugEventListener(
ComponentTreeDebugEventListener componentTreeDebugEventListener) {
this.componentTreeDebugEventListener = componentTreeDebugEventListener;
return this;
}

/**
* Whether or not to enable the incremental mount optimization. True by default.
*
Expand Down
83 changes: 83 additions & 0 deletions litho-core/src/main/java/com/facebook/litho/LithoEventListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.facebook.litho

import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEventSubscriber

/**
* This is used as a listener for events that happen on the scope of a given [ComponentTree]. These
* events can be related with both the `render` UI pipeline, or with the view-side events created by
* rendercore in the [LithoView] that is associated to the [ComponentTree] to which this listener
* observes.
*
* In order to specify which events you are interested in, you should override the
* [ComponentTreeDebugEventListener.events] and list any of the events present in [LithoDebugEvent]
* or [DebugEvent].
*
* A client can attach one of these listeners during the creation of a [ComponentTree]:
* ```
* val listener = object: ComponentTreeDebugEventListener {
* override fun onEvent(debugEvent: DebugEvent) {
* /* do your logging / business logic */
* }
*
* override val events = setOf(LayoutCommitted, MountItemMount)
* }
*
* val componentTree = ComponentTree.create(context, MyComponent())
* .withComponentTreeDebugEventListener(listener)
* .create()
*
* val lithoView = LithoView.create(context, componentTree)
* ```
*/
interface ComponentTreeDebugEventListener {

fun onEvent(debugEvent: DebugEvent)

val events: Set<String>
}

/**
* This abstraction acts as a wrapper which guarantees that it will act only in events related to
* the [componentTreeId].
*
* This is meant to be used only by the internals of the [ComponentTree], by wrapping any
* [ComponentTreeDebugEventListener] passed in by the clients. This is a mechanism so that we can
* guarantee that the client's listener will only observe events related to the [ComponentTree] to
* which it is associated.
*
* *Note:* ideally this code would belong inside the [ComponentTree], but to keep it in Kotlin we
* are leaving it outside it until finish the Kotlin migration.
*/
internal class ComponentTreeDebugEventsSubscriber(
componentTreeId: Int,
eventsToObserve: Set<String>,
private val onComponentTreeEvent: (DebugEvent) -> Unit,
) : DebugEventSubscriber(*eventsToObserve.toTypedArray()) {

private val componentTreeIdStr = componentTreeId.toString()

override fun onEvent(event: DebugEvent) {
if (event.renderStateId != componentTreeIdStr) {
return
}

onComponentTreeEvent(event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.facebook.litho

import android.content.Context
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.facebook.litho.debug.LithoDebugEvent.LayoutCommitted
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEventBus
import com.facebook.rendercore.debug.DebugEventDispatcher
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(LithoTestRunner::class)
class ComponentTreeDebugEventListenerTest {

@get:Rule val rule = LithoViewRule()

@Before
fun setup() {
DebugEventBus.enabled = true
}

@After
fun tearDown() {
DebugEventBus.enabled = false
}

@Test
fun `should process events if associated with a ComponentTree`() {
val listener = TestListener()

val componentContext = ComponentContext(getApplicationContext() as Context)
val componentTree =
ComponentTree.create(componentContext, EmptyComponent())
.withComponentTreeDebugEventListener(listener)
.build()

rule.render(componentTree = componentTree) { MyComponent() }
rule.idle()

assertThat(listener.observedEvents).hasSize(1)
assertThat(listener.observedEvents.first().type).isEqualTo(LayoutCommitted)
}

@Test
fun `should not process events if not associated with a ComponentTree`() {
val listener = TestListener()

val componentContext = ComponentContext(getApplicationContext() as Context)
val componentTree =
ComponentTree.create(componentContext, EmptyComponent())
.withComponentTreeDebugEventListener(null)
.build()

rule.render(componentTree = componentTree) { MyComponent() }
rule.idle()

assertThat(listener.observedEvents).hasSize(0)
}

@Test
fun `should not process any component tree event once released`() {
val listener = TestListener()

val componentContext = ComponentContext(getApplicationContext() as Context)
val componentTree =
ComponentTree.create(componentContext, EmptyComponent())
.withComponentTreeDebugEventListener(listener)
.build()

rule.render(componentTree = componentTree) { MyComponent() }
rule.idle()

assertThat(listener.observedEvents).hasSize(1)
assertThat(listener.observedEvents.last().type).isEqualTo(LayoutCommitted)

/* if we dispatch the event we are looking for we will process it */
DebugEventDispatcher.dispatch(
type = LayoutCommitted, renderStateId = { componentTree.mId.toString() })
assertThat(listener.observedEvents).hasSize(2)
assertThat(listener.observedEvents.last().type).isEqualTo(LayoutCommitted)

/* if we dispatch the event we are looking for after the Component Tree is released, it will not
be processed */
componentTree.release()
DebugEventDispatcher.dispatch(
type = LayoutCommitted, renderStateId = { componentTree.mId.toString() })
assertThat(listener.observedEvents).hasSize(2)
}

private class TestListener : ComponentTreeDebugEventListener {

val observedEvents: List<DebugEvent>
get() = _observedEvents

private val _observedEvents = mutableListOf<DebugEvent>()

override fun onEvent(debugEvent: DebugEvent) {
_observedEvents.add(debugEvent)
}

override val events: Set<String> = setOf(LayoutCommitted)
}

private class MyComponent : KComponent() {
override fun ComponentScope.render(): Component = Text("Hello")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@
package com.facebook.samples.litho.kotlin.logging

import android.os.Bundle
import android.util.Log
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentTree
import com.facebook.litho.ComponentTreeDebugEventListener
import com.facebook.litho.LithoView
import com.facebook.litho.debug.LithoDebugEvent.LayoutCommitted
import com.facebook.litho.debug.LithoDebugEvent.StateUpdateEnqueued
import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEvent.Companion.MountItemMount
import com.facebook.samples.litho.NavigatableDemoActivity

class LoggingActivity : NavigatableDemoActivity() {
Expand All @@ -27,6 +34,21 @@ class LoggingActivity : NavigatableDemoActivity() {
super.onCreate(savedInstanceState)

val c = ComponentContext(this, "LITHOSAMPLE", SampleComponentsLogger())
setContentView(LithoView.create(c, LoggingRootComponent()))
val lithoView =
LithoView.create(
c,
ComponentTree.create(c, LoggingRootComponent())
.withComponentTreeDebugEventListener(
object : ComponentTreeDebugEventListener {
override fun onEvent(debugEvent: DebugEvent) {
Log.d("litho-events", debugEvent.toString())
}

override val events: Set<String> =
setOf(MountItemMount, StateUpdateEnqueued, LayoutCommitted)
})
.build())

setContentView(lithoView)
}
}

0 comments on commit d71cdbe

Please sign in to comment.