Skip to content

Commit

Permalink
Hermes perf optimization (#598)
Browse files Browse the repository at this point in the history
* automatic function serialization

* lint

* flowInstance transition

* more lint

* get doesn't work

* nodeserializablefunction update

* flowController transition

* constants controller function cache

* lint and change test equals back

* expression eval and beacon

* logger and playerFlowState

* the rest of the getInvokables
  • Loading branch information
brocollie08 authored Feb 27, 2025
1 parent e9fa10d commit 4f29d3b
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.intuit.playerui.core.bridge.serialization.serializers

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.serialization.format.serializer
import com.intuit.playerui.core.experimental.ExperimentalPlayerApi
import kotlinx.serialization.DeserializationStrategy
import kotlin.reflect.KProperty

/** Delegate for automatic deserialization of [Node] values */
internal class NodeSerializableFunction<R> private constructor(
private val provider: () -> Node,
private val serializer: DeserializationStrategy<R>,
internal val strategy: CacheStrategy,
private val name: String?,
) {

/** Caching strategy for determining how to pull the value from [Node] on subsequent attempts */
public enum class CacheStrategy {
None,
Full,
}

/** Cache of container [Node] that will reset the [value] cache if out-of-date with the [provider] */
private var cache: Node = provider(); get() {
val provided = provider()
field = provided
value = null

return field
}

/** Cache of the [T] value, along with the backing [Node] for objects */
private var value: Invokable<R>? = null

public operator fun getValue(thisRef: Any?, property: KProperty<*>): Invokable<R> {
// early exit if we have a value and explicitly using the cache
value?.takeIf { strategy == CacheStrategy.Full }?.let {
return it
}

val key = name ?: property.name

// will reset cache and value if mismatch
val node = cache

// else get and deserialize the value
return node.getInvokable(key, serializer)!!
}

public companion object {

/** Smart constructor responsible for determining the correct [CacheStrategy] and [defaultValue] from the [serializer], if either are not provided */
@ExperimentalPlayerApi
public operator fun <R> invoke(
provider: () -> Node,
serializer: DeserializationStrategy<R>,
strategy: CacheStrategy? = null,
name: String? = null,
): NodeSerializableFunction<R> = NodeSerializableFunction(
provider,
serializer,
strategy ?: CacheStrategy.Full,
name,
)
}
}

@ExperimentalPlayerApi
internal fun <R> NodeWrapper.NodeSerializableFunction(
serializer: DeserializationStrategy<R>,
strategy: NodeSerializableFunction.CacheStrategy? = null,
name: String? = null,
defaultValue: (Node.(String) -> Invokable<R>)? = null,
): NodeSerializableFunction<R> = NodeSerializableFunction(::node, serializer, strategy, name)

@ExperimentalPlayerApi
internal inline fun <reified R> NodeWrapper.NodeSerializableFunction(
strategy: NodeSerializableFunction.CacheStrategy? = null,
name: String? = null,
noinline defaultValue: (Node.(String) -> Invokable<R>)? = null,
): NodeSerializableFunction<R> = NodeSerializableFunction(node.format.serializer<R>(), strategy, name, defaultValue)
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.intuit.playerui.core.constants
import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import kotlinx.serialization.Serializable

@Serializable(with = ConstantsController.Serializer::class)
public class ConstantsController(override val node: Node) : NodeWrapper {
private val addConstants: Invokable<Unit>? by NodeSerializableFunction()
private val getConstants: Invokable<Any?>? by NodeSerializableFunction()
private val setTemporaryValues: Invokable<Unit>? by NodeSerializableFunction()
private val clearTemporaryValues: Invokable<Unit>? by NodeSerializableFunction()

/**
* Function to add constants to the providers store
* @param data values to add to the constants store
* @param namespace namespace to add the constants under
*/
public fun addConstants(data: Map<String, Any>, namespace: String) {
node.getInvokable<Unit>("addConstants")?.invoke(data, namespace)
addConstants?.invoke(data, namespace)
}

/**
Expand All @@ -23,7 +29,7 @@ public class ConstantsController(override val node: Node) : NodeWrapper {
* @param fallback Optional - if key doesn't exist in namespace what to return (will return unknown if not provided)
*/
public fun getConstants(key: String, namespace: String, fallback: Any? = null): Any? {
return node.getInvokable<Any?>("getConstants")?.invoke(key, namespace, fallback)
return getConstants?.invoke(key, namespace, fallback)
}

/**
Expand All @@ -32,14 +38,14 @@ public class ConstantsController(override val node: Node) : NodeWrapper {
* @param namespace namespace to override
*/
public fun setTemporaryValues(data: Any, namespace: String) {
node.getInvokable<Unit>("setTemporaryValues")?.invoke(data, namespace)
setTemporaryValues?.invoke(data, namespace)
}

/**
* Clears any temporary values that were previously set
*/
public fun clearTemporaryValues() {
node.getInvokable<Unit>("clearTemporaryValues")?.invoke()
clearTemporaryValues?.invoke()
}

internal object Serializer : NodeWrapperSerializer<ConstantsController>(::ConstantsController)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
package com.intuit.playerui.core.data

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import com.intuit.playerui.core.data.DataController.Serializer
import kotlinx.serialization.Serializable

/** Limited definition of the player data controller to enable data modification */
@Serializable(with = Serializer::class)
public class DataController internal constructor(override val node: Node) : NodeWrapper {

private val set: Invokable<Unit>? by NodeSerializableFunction()
private val get: Invokable<Any?>? by NodeSerializableFunction()

/** Apply [data] to the underlying data model */
public fun set(data: Map<String, Any?>) {
node.getInvokable<Unit>("set")?.invoke(data)
set?.invoke(data)
}

/** [set] each of the [Binding]s contained in the [transaction] */
public fun set(transaction: List<List<Any?>>) {
// TODO: this unfortunately doesn't work yet because it's also got the name "set"
node.getInvokable<Unit>("set")?.invoke(transaction)
}

public fun get(binding: Binding): Any? = node.getInvokable<Any?>("get")?.invoke(binding)
public fun get(binding: Binding): Any? = get?.invoke(binding)

internal object Serializer : NodeWrapperSerializer<DataController>(::DataController)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.intuit.playerui.core.data

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import com.intuit.playerui.core.data.DataModelWithParser.Serializer
import kotlinx.serialization.Serializable
Expand All @@ -12,14 +13,17 @@ import kotlinx.serialization.Serializable
/** Data model handle that provides [get] and [set] functionality w/ binding resolution */
@Serializable(Serializer::class)
public class DataModelWithParser internal constructor(override val node: Node) : NodeWrapper {
private val get: Invokable<Any?>? by NodeSerializableFunction()
private val set: Invokable<Unit>? by NodeSerializableFunction()

/** Retrieve specific section of the data model resolved from the [binding] */
public fun get(binding: Binding): Any? {
return node.getInvokable<Any?>("get")?.invoke(binding)
return get?.invoke(binding)
}

/** [set] each of the [Binding]s contained in the [transaction] */
public fun set(transaction: List<List<Any?>>) {
node.getInvokable<Unit>("set")?.invoke(transaction)
set?.invoke(transaction)
}

internal object Serializer : NodeWrapperSerializer<DataModelWithParser>(::DataModelWithParser)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.intuit.playerui.core.expressions

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -30,8 +31,8 @@ public fun ExpressionEvaluator.evaluate(expression: Expression): Any? = when (ex

@Serializable(ExpressionController.Serializer::class)
public class ExpressionController(override val node: Node) : NodeWrapper, ExpressionEvaluator {
override fun evaluate(expressions: List<String>): Any? = node
.getInvokable<Any>("evaluate")?.invoke(expressions)
private val evaluate: Invokable<Any>? by NodeSerializableFunction()
override fun evaluate(expressions: List<String>): Any? = evaluate?.invoke(expressions)

internal object Serializer : NodeWrapperSerializer<ExpressionController>(::ExpressionController)
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.intuit.playerui.core.flow

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.hooks.NodeSyncHook1
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer

/** Limited definition of the player flow controller that enables flow transitions */
@Serializable(with = FlowController.Serializer::class)
public class FlowController internal constructor(override val node: Node) : NodeWrapper, Transition {
private val transition: Invokable<Unit>? by NodeSerializableFunction()

override fun transition(state: String, options: TransitionOptions?) {
node.getInvokable<Unit>("transition")?.invoke(state, options)
transition?.invoke(state, options)
}

public val hooks: Hooks by NodeSerializableField(Hooks.serializer())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package com.intuit.playerui.core.flow

import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.hooks.NodeSyncBailHook1
import com.intuit.playerui.core.bridge.hooks.NodeSyncHook1
import com.intuit.playerui.core.bridge.hooks.NodeSyncHook2
import com.intuit.playerui.core.bridge.hooks.NodeSyncWaterfallHook1
import com.intuit.playerui.core.bridge.hooks.NodeSyncWaterfallHook2
import com.intuit.playerui.core.bridge.serialization.serializers.GenericSerializer
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import com.intuit.playerui.core.flow.state.NavigationFlowState
import com.intuit.playerui.core.player.state.NamedState
Expand All @@ -26,8 +27,10 @@ public class FlowInstance(override val node: Node) : NodeWrapper, Transition {

public val currentState: NamedState? by NodeSerializableField(NamedState.serializer().nullable)

private val transition: Invokable<Unit>? by NodeSerializableFunction()

override fun transition(state: String, options: TransitionOptions?) {
node.getInvokable<Unit>("transition")?.invoke(state, options)
transition?.invoke(state, options)
}

@Serializable(Hooks.Serializer::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.intuit.playerui.core.logger

import com.intuit.hooks.HookContext
import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.hooks.NodeSyncHook1
import com.intuit.playerui.core.bridge.hooks.SyncHook1
import com.intuit.playerui.core.bridge.runtime
import com.intuit.playerui.core.bridge.serialization.serializers.GenericSerializer
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import com.intuit.playerui.core.plugins.LoggerPlugin
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -36,33 +37,39 @@ public class TapableLogger(override val node: Node) : LoggerPlugin, NodeWrapper

public val hooks: Hooks by NodeSerializableField(Hooks.serializer())

private val trace: Invokable<Unit> by NodeSerializableFunction()
private val debug: Invokable<Unit> by NodeSerializableFunction()
private val info: Invokable<Unit> by NodeSerializableFunction()
private val warn: Invokable<Unit> by NodeSerializableFunction()
private val error: Invokable<Unit> by NodeSerializableFunction()

public override fun trace(vararg args: Any?) {
runtime.scope.launch {
node.getInvokable<Unit>("trace")!!(*args)
trace.invoke(*args)
}
}

public override fun debug(vararg args: Any?) {
runtime.scope.launch {
node.getInvokable<Unit>("debug")!!(*args)
debug.invoke(*args)
}
}

public override fun info(vararg args: Any?) {
runtime.scope.launch {
node.getInvokable<Unit>("info")!!(*args)
info.invoke(*args)
}
}

public override fun warn(vararg args: Any?) {
runtime.scope.launch {
node.getInvokable<Unit>("warn")!!(*args)
warn.invoke(*args)
}
}

public override fun error(vararg args: Any?) {
runtime.scope.launch {
node.getInvokable<Unit>("error")!!(*args)
error.invoke(*args)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package com.intuit.playerui.core.player.state
import com.intuit.playerui.core.asset.Asset
import com.intuit.playerui.core.bridge.Completable
import com.intuit.playerui.core.bridge.EmptyNode
import com.intuit.playerui.core.bridge.Invokable
import com.intuit.playerui.core.bridge.Node
import com.intuit.playerui.core.bridge.NodeWrapper
import com.intuit.playerui.core.bridge.Promise
import com.intuit.playerui.core.bridge.deserialize
import com.intuit.playerui.core.bridge.getInvokable
import com.intuit.playerui.core.bridge.getSerializable
import com.intuit.playerui.core.bridge.getSymbol
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField
import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction
import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer
import com.intuit.playerui.core.data.DataController
import com.intuit.playerui.core.data.DataModelWithParser
Expand Down Expand Up @@ -125,6 +126,8 @@ public class InProgressState internal constructor(override val node: Node) :

override val status: PlayerFlowStatus = IN_PROGRESS

private val fail: Invokable<Any> by NodeSerializableFunction()

/** [FlowResult] value that will be available once the flow completes */
// TODO: Make non-nullable if possible - requires Promise change
public val flowResult: Completable<FlowResult?> get() = Promise(
Expand All @@ -134,7 +137,7 @@ public class InProgressState internal constructor(override val node: Node) :
public val controllers: ControllerState by NodeSerializableField(ControllerState.serializer())

public fun fail(error: Throwable) {
node.getInvokable<Any>("fail")!!.invoke(error)
fail.invoke(error)
}

internal object Serializer : NodeWrapperSerializer<InProgressState>(::InProgressState, IN_PROGRESS.value)
Expand Down
Loading

0 comments on commit 4f29d3b

Please sign in to comment.