You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, when you render children or run workers, both are compared between render passes to determine if they were already rendered/ran on the last pass. But the comparison is implemented differently for workflows and workers: Workflows are compared by concrete type, whereas Workers are compared by the doesSameWorkAs method. Implementations of Worker are responsible for comparing by type.
This approach has a number of issues:
Consistency: You can render two Workflow<Foo, Bar, Baz> workflows that happen to have different type implementations, and it's fine. You can run two Worker<Fizz> workers and the implementation of the first one's doesSameWorkAs determines if that is valid or not. The mental model is complicated.
Abstraction: If you want to build a generic workflow that renders some children, and the children are defined by generic types, it's not clear whether the children need keys or not to be deduped.
Efficiency: Since the runtime has no knowledge of worker types, the only way to compare workers is to do a linear search of the previous ones. The Swift implementation, which does know everything about worker type parameters, uses those types to store workers in a map and do constant-time lookups, which is much more efficient for large numbers of workers.
Proposed Solution
I think we can solve most of these problems in Kotlin using a special Kotlin-only reflection feature called KType. A KType is similar to a Type in java. It represents not only a KClass, but also all the type parameters and their variances and bounds. Since this information is only known at compile time, a KType can only be derived at compile-time, using the inline function typeOf<Foo>().
Implementation Strategy
Make the core renderChild and runningWorker methods take an additional KType parameter for their workflow/worker types. These methods should be internal, since they would be very easy to misuse if public (KType has no type parameters of its own, and we can't control what you pass in).
Since interfaces can't have internal/protected methods, make RenderContext an abstract class instead of an interface.
Convert the current core renderChild and runningWorker methods to extension functions, siblings to all the other convenience overloads.
Make all public-facing renderChild/runningWorker inline, so they can use typeOf.
These steps can be done incrementally, e.g. #908 converted TypedWorker to use KType instead of KClass. The rest of the worker API could be converted next, followed by workflows.
Further Work
Once this is done, a few more changes could be made:
Workers could be keyed by their type+key, like in Swift, since the runtime now always knows worker types.
The worker conversion functions for reactive types would no longer need to be inline, since they don't need to know their own types.
Open Questions
Currently the snapshotting machinery writes KClass names for workflows, so it can look up those KClasses again on rehydration and use them as keys for the snapshot cache.
I think to solve this we can just store either the KType.toString()or the KType itself, and compare whatever we have available. The implementation should be mutable so that equivalent WorkflowIds will converge on all pointing to the same objects, to make comparisons more efficient.
Infuriatingly, Nothing can't be used as a reified type parameter, which means rendering Workflow<*, Nothing, *> and running Worker<Nothing> would need some tricks.
I'm not sure what the runtime performance overhead of calling typeOf all the time is. Worst case it's allocating a whole bunch of reflection-y objects on every call, which would happen many times per render pass. If instead it generates that tree statically at compile time, it's not going to be that expensive. Need to check the generated bytecode.
The text was updated successfully, but these errors were encountered:
Background
Currently, when you render children or run workers, both are compared between render passes to determine if they were already rendered/ran on the last pass. But the comparison is implemented differently for workflows and workers: Workflows are compared by concrete type, whereas Workers are compared by the
doesSameWorkAs
method. Implementations of Worker are responsible for comparing by type.This approach has a number of issues:
Workflow<Foo, Bar, Baz>
workflows that happen to have different type implementations, and it's fine. You can run twoWorker<Fizz>
workers and the implementation of the first one'sdoesSameWorkAs
determines if that is valid or not. The mental model is complicated.Proposed Solution
I think we can solve most of these problems in Kotlin using a special Kotlin-only reflection feature called
KType
. AKType
is similar to aType
in java. It represents not only aKClass
, but also all the type parameters and their variances and bounds. Since this information is only known at compile time, aKType
can only be derived at compile-time, using the inline functiontypeOf<Foo>()
.Implementation Strategy
renderChild
andrunningWorker
methods take an additionalKType
parameter for their workflow/worker types. These methods should be internal, since they would be very easy to misuse if public (KType
has no type parameters of its own, and we can't control what you pass in).RenderContext
an abstract class instead of an interface.renderChild
andrunningWorker
methods to extension functions, siblings to all the other convenience overloads.renderChild
/runningWorker
inline, so they can usetypeOf
.These steps can be done incrementally, e.g. #908 converted
TypedWorker
to useKType
instead ofKClass
. The rest of the worker API could be converted next, followed by workflows.Further Work
Once this is done, a few more changes could be made:
Open Questions
KClass
names for workflows, so it can look up thoseKClass
es again on rehydration and use them as keys for the snapshot cache.KType.toString()
or theKType
itself, and compare whatever we have available. The implementation should be mutable so that equivalentWorkflowId
s will converge on all pointing to the same objects, to make comparisons more efficient.Nothing
can't be used as areified
type parameter, which means renderingWorkflow<*, Nothing, *>
and runningWorker<Nothing>
would need some tricks.typeOf
all the time is. Worst case it's allocating a whole bunch of reflection-y objects on every call, which would happen many times per render pass. If instead it generates that tree statically at compile time, it's not going to be that expensive. Need to check the generated bytecode.The text was updated successfully, but these errors were encountered: