From 1010184624a91edc2e0b08f815e2b3e4c2352952 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 26 Aug 2024 18:34:06 -0400 Subject: [PATCH] Optimize Modifier serialization in guest code (#2253) * Optimize Modifier serialization in guest code I learned that encoding a type as a JsonElement and then encoding that as JSON is significantly less efficient than going directly from the type to JSON. This is due to some inefficient code in the internals of kotlinx.serialization. Skipping this intermediate step is useful on its own, but it also turns out to be significantly more efficient in practice. * Fix sample code * Delete unused generated code * Drop an obsolete test * Return a pair from ProtocolWidgetSystemFactory * Generate the tag and serializer as a single symbol * Don't encode ',null' unnecessarily --- CHANGELOG.md | 1 + .../api/redwood-protocol-guest.api | 5 +- .../api/redwood-protocol-guest.klib.api | 5 +- .../guest/DefaultGuestProtocolAdapter.kt | 20 +- .../protocol/guest/GuestProtocolAdapter.kt | 4 +- .../guest/ProtocolWidgetSystemFactory.kt | 10 + .../tooling/codegen/protocolCodegen.kt | 1 - .../codegen/protocolGuestGeneration.kt | 196 ++++++++---------- .../app/cash/redwood/tooling/codegen/types.kt | 3 +- .../codegen/ProtocolGuestGenerationTest.kt | 13 -- .../redwood/treehouse/ProtocolBridgeJs.kt | 25 ++- .../treehouse/FastGuestProtocolAdapterTest.kt | 16 ++ 12 files changed, 161 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2b2b1023..3fba30de6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ New: - Source-based schema parser is now the default. The `useFir` Gradle property has been removed. - Introduce a `LoadingStrategy` interface to manage `LazyList` preloading. +- Optimize encoding modifiers in Kotlin/JS. Changed: - In Treehouse, events from the UI are now serialized on a background thread. This means that there is both a delay and a thread change between when a UI binding sends an event and when that object is converted to JSON. All arguments to events must not be mutable and support property reads on any thread. Best practice is for all event arguments to be completely immutable. diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.api b/redwood-protocol-guest/api/redwood-protocol-guest.api index 45d7c04a9c..4a14797de5 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.api @@ -4,7 +4,7 @@ public final class app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter : public synthetic fun (Lkotlinx/serialization/json/Json;Ljava/lang/String;Lapp/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun appendAdd-ARs5Qwk (IIILapp/cash/redwood/protocol/guest/ProtocolWidget;)V public fun appendCreate-kyz2zXs (II)V - public fun appendModifierChange-z3jyS0k (ILjava/util/List;)V + public fun appendModifierChange-z3jyS0k (ILapp/cash/redwood/Modifier;)V public fun appendMove-HpxY78w (IIIII)V public fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V public fun appendPropertyChange-M7EZMwg (III)V @@ -25,7 +25,7 @@ public final class app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter : public abstract interface class app/cash/redwood/protocol/guest/GuestProtocolAdapter : app/cash/redwood/protocol/EventSink { public abstract fun appendAdd-ARs5Qwk (IIILapp/cash/redwood/protocol/guest/ProtocolWidget;)V public abstract fun appendCreate-kyz2zXs (II)V - public abstract fun appendModifierChange-z3jyS0k (ILjava/util/List;)V + public abstract fun appendModifierChange-z3jyS0k (ILapp/cash/redwood/Modifier;)V public abstract fun appendMove-HpxY78w (IIIII)V public abstract fun appendPropertyChange-DxQz5cw (IILkotlinx/serialization/KSerializer;Ljava/lang/Object;)V public abstract fun appendPropertyChange-M7EZMwg (III)V @@ -81,6 +81,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/ public abstract interface class app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory { public abstract fun create (Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/widget/WidgetSystem; public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory;Lapp/cash/redwood/protocol/guest/GuestProtocolAdapter;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/widget/WidgetSystem; + public abstract fun modifierTagAndSerializationStrategy (Lapp/cash/redwood/Modifier$Element;)Lkotlin/Pair; } public final class app/cash/redwood/protocol/guest/VersionKt { diff --git a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api index 180abbd8b8..257fa3136f 100644 --- a/redwood-protocol-guest/api/redwood-protocol-guest.klib.api +++ b/redwood-protocol-guest/api/redwood-protocol-guest.klib.api @@ -19,7 +19,7 @@ abstract interface app.cash.redwood.protocol.guest/GuestProtocolAdapter : app.ca abstract fun <#A1: kotlin/Any?> appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlinx.serialization/KSerializer<#A1>, #A1) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlinx.serialization.KSerializer<0:0>;0:0){0§}[0] abstract fun appendAdd(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, app.cash.redwood.protocol.guest/ProtocolWidget) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendAdd|appendAdd(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;app.cash.redwood.protocol.guest.ProtocolWidget){}[0] abstract fun appendCreate(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendCreate|appendCreate(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0] - abstract fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] + abstract fun appendModifierChange(app.cash.redwood.protocol/Id, app.cash.redwood/Modifier) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;app.cash.redwood.Modifier){}[0] abstract fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] abstract fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] @@ -53,6 +53,7 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolWidget : app.cash.red } abstract interface app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory { // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory|null[0] + abstract fun <#A1: app.cash.redwood/Modifier.Element> modifierTagAndSerializationStrategy(#A1): kotlin/Pair?> // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.modifierTagAndSerializationStrategy|modifierTagAndSerializationStrategy(0:0){0§}[0] abstract fun create(app.cash.redwood.protocol.guest/GuestProtocolAdapter, app.cash.redwood.protocol.guest/ProtocolMismatchHandler = ...): app.cash.redwood.widget/WidgetSystem // app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory.create|create(app.cash.redwood.protocol.guest.GuestProtocolAdapter;app.cash.redwood.protocol.guest.ProtocolMismatchHandler){}[0] } @@ -71,7 +72,7 @@ final class app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter : app.ca final fun <#A1: kotlin/Any?> appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlinx.serialization/KSerializer<#A1>, #A1) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlinx.serialization.KSerializer<0:0>;0:0){0§}[0] final fun appendAdd(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, app.cash.redwood.protocol.guest/ProtocolWidget) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendAdd|appendAdd(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;app.cash.redwood.protocol.guest.ProtocolWidget){}[0] final fun appendCreate(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/WidgetTag) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendCreate|appendCreate(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.WidgetTag){}[0] - final fun appendModifierChange(app.cash.redwood.protocol/Id, kotlin.collections/List) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;kotlin.collections.List){}[0] + final fun appendModifierChange(app.cash.redwood.protocol/Id, app.cash.redwood/Modifier) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendModifierChange|appendModifierChange(app.cash.redwood.protocol.Id;app.cash.redwood.Modifier){}[0] final fun appendMove(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Int) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendMove|appendMove(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/Boolean) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.Boolean){}[0] final fun appendPropertyChange(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/PropertyTag, kotlin/UInt) // app.cash.redwood.protocol.guest/DefaultGuestProtocolAdapter.appendPropertyChange|appendPropertyChange(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.PropertyTag;kotlin.UInt){}[0] diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt index b766196077..a5c5b70ced 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.ChangesSink @@ -41,7 +42,7 @@ import kotlinx.serialization.json.JsonPrimitive public class DefaultGuestProtocolAdapter( public override val json: Json = Json.Default, hostVersion: RedwoodVersion, - widgetSystemFactory: ProtocolWidgetSystemFactory, + private val widgetSystemFactory: ProtocolWidgetSystemFactory, private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ) : GuestProtocolAdapter { private var nextValue = Id.Root.value + 1 @@ -111,13 +112,22 @@ public class DefaultGuestProtocolAdapter( changes.add(PropertyChange(id, tag, JsonPrimitive(value))) } - public override fun appendModifierChange( - id: Id, - elements: List, - ) { + override fun appendModifierChange(id: Id, value: Modifier) { + val elements = mutableListOf() + + value.forEach { element -> + elements += modifierElement(element) + } + changes.add(ModifierChange(id, elements)) } + private fun modifierElement(element: T): ModifierElement { + val (tag, serializer) = widgetSystemFactory.modifierTagAndSerializationStrategy(element) + if (serializer == null) return ModifierElement(tag) + return ModifierElement(tag, json.encodeToJsonElement(serializer, element)) + } + public override fun appendAdd( id: Id, tag: ChildrenTag, diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt index 1c16de6d93..40300dbda1 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/GuestProtocolAdapter.kt @@ -15,12 +15,12 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.ChangesSink import app.cash.redwood.protocol.ChildrenTag import app.cash.redwood.protocol.EventSink import app.cash.redwood.protocol.Id -import app.cash.redwood.protocol.ModifierElement import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.WidgetTag import app.cash.redwood.widget.Widget @@ -106,7 +106,7 @@ public interface GuestProtocolAdapter : EventSink { @RedwoodCodegenApi public fun appendModifierChange( id: Id, - elements: List, + value: Modifier, ) @RedwoodCodegenApi diff --git a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt index e8eef5ac70..efa42ad287 100644 --- a/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt +++ b/redwood-protocol-guest/src/commonMain/kotlin/app/cash/redwood/protocol/guest/ProtocolWidgetSystemFactory.kt @@ -15,7 +15,11 @@ */ package app.cash.redwood.protocol.guest +import app.cash.redwood.Modifier +import app.cash.redwood.RedwoodCodegenApi +import app.cash.redwood.protocol.ModifierTag import app.cash.redwood.widget.WidgetSystem +import kotlinx.serialization.SerializationStrategy public interface ProtocolWidgetSystemFactory { /** Create a new [WidgetSystem] connected to a host via [guestAdapter]. */ @@ -23,4 +27,10 @@ public interface ProtocolWidgetSystemFactory { guestAdapter: GuestProtocolAdapter, mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ): WidgetSystem + + /** The serialization strategy is null if the modifier is stateless. */ + @RedwoodCodegenApi + public fun modifierTagAndSerializationStrategy( + element: T, + ): Pair?> } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt index 8e73107c0f..90eb1eb393 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolCodegen.kt @@ -37,7 +37,6 @@ internal fun ProtocolSchemaSet.generateFileSpecs(type: ProtocolCodegenType): Lis when (type) { Guest -> { add(generateProtocolWidgetSystemFactory(this@generateFileSpecs)) - add(generateComposeProtocolModifierSerialization(this@generateFileSpecs)) for (dependency in all) { add(generateProtocolWidgetFactory(dependency, host = schema)) generateProtocolModifierSerializers(dependency, host = schema)?.let { add(it) } diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt index 5bdf2539a3..6e939ead27 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/protocolGuestGeneration.kt @@ -38,17 +38,17 @@ import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.KModifier.INTERNAL import com.squareup.kotlinpoet.KModifier.OVERRIDE import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.KModifier.PUBLIC import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LambdaTypeName import com.squareup.kotlinpoet.MemberName -import com.squareup.kotlinpoet.NOTHING import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.SHORT import com.squareup.kotlinpoet.STRING import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.UNIT import com.squareup.kotlinpoet.U_INT import com.squareup.kotlinpoet.joinToCode @@ -66,6 +66,12 @@ object ExampleProtocolWidgetSystemFactory : ProtocolWidgetSystemFactory { RedwoodLayout = ProtocolRedwoodLayoutWidgetFactory(guestAdapter, mismatchHandler), ) } + + public override fun modifierTag(element: Modifier.Element): ModifierTag { ... } + + public override fun modifierTagAndSerializationStrategy( + element: T + ): Pair?> { ... } } */ internal fun generateProtocolWidgetSystemFactory( @@ -96,6 +102,7 @@ internal fun generateProtocolWidgetSystemFactory( } .build(), ) + .addFunction(modifierTagAndSerializationStrategy(schemaSet)) .build(), ) } @@ -197,14 +204,11 @@ internal class ProtocolButton( override var modifier: Modifier get() = throw AssertionError() set(value) { - val json = buildJsonArray { - value.forEach { element -> add(element.toJsonElement(guestAdapter.json)) - } - guestAdapter.appendModifierChange(id, guestAdapter.json)) + guestAdapter.appendModifierChange(id, value) } override fun text(text: String?) { - guestAdapter.appendPropertyChange(this.id, PropertyTag(1), guestAdapter.json.encodeToJsonElement(serializer_0, text)) + this.guestAdapter.appendPropertyChange(this.id, PropertyTag(1), serializer_0, text) } override fun onClick(onClick: (() -> Unit)?) { @@ -417,7 +421,7 @@ internal fun generateProtocolWidget( .setter( FunSpec.setterBuilder() .addParameter("value", Redwood.Modifier) - .addStatement("guestAdapter.appendModifierChange(id, value.%M(guestAdapter.json))", host.modifierToProtocol) + .addStatement("guestAdapter.appendModifierChange(id, value)") .build(), ) .build(), @@ -476,7 +480,8 @@ private fun workAroundLazyListPlaceholderRemoveCrash( ): Boolean = widget.type.names in placeholderParentTypeNames && trait.name == "placeholder" /* -internal object GrowSerializer : KSerializer { +internal val GrowTagAndSerializer: Pair?> = + ModifierTag(1_000_001) to object : SerializationStrategy { override val descriptor = buildClassSerialDescriptor("app.cash.redwood.layout.Grow") { element("value") @@ -491,26 +496,20 @@ internal object GrowSerializer : KSerializer { override fun deserialize(decoder: Decoder): Grow { throw AssertionError() } - - fun encode(json: Json, value: Grow): ModifierElement { - val element = json.encodeToJsonElement(this, value) - return ModifierElement(ModifierTag(3), element) - } } */ internal fun generateProtocolModifierSerializers( schema: ProtocolSchema, host: ProtocolSchema, ): FileSpec? { - val serializableModifiers = schema.modifiers.filter { it.properties.isNotEmpty() } - if (serializableModifiers.isEmpty()) { + if (schema.modifiers.isEmpty()) { return null } return buildFileSpec(schema.guestProtocolPackage(host), "modifierSerializers") { addAnnotation(suppressDeprecations) - for (modifier in serializableModifiers) { - val serializerType = schema.modifierSerializer(modifier, host) + for (modifier in schema.modifiers) { + val serializerTagAndSerializer = schema.modifierTagAndSerializer(modifier, host) val modifierType = schema.modifierType(modifier) var nextSerializerId = 0 @@ -606,10 +605,11 @@ internal fun generateProtocolModifierSerializers( } } - addType( - TypeSpec.objectBuilder(serializerType) - .addModifiers(INTERNAL) - .addSuperinterface(KotlinxSerialization.KSerializer.parameterizedBy(modifierType)) + val serializationStrategyType = + KotlinxSerialization.SerializationStrategy.parameterizedBy(modifierType) + val serializationStrategyValue = if (modifier.properties.isNotEmpty()) { + TypeSpec.anonymousClassBuilder() + .addSuperinterface(serializationStrategyType) .addProperty( PropertySpec.builder("descriptor", KotlinxSerialization.SerialDescriptor) .addModifiers(OVERRIDE) @@ -640,7 +640,11 @@ internal fun generateProtocolModifierSerializers( parameters += CodeBlock.of("emptyArray()") } - initializer("%T(%L)", KotlinxSerialization.ContextualSerializer, parameters.joinToCode()) + initializer( + "%T(%L)", + KotlinxSerialization.ContextualSerializer, + parameters.joinToCode(), + ) } .build(), ) @@ -661,27 +665,25 @@ internal fun generateProtocolModifierSerializers( .addStatement("composite.endStructure(descriptor)") .build(), ) - .addFunction( - FunSpec.builder("deserialize") - .addModifiers(OVERRIDE) - .addParameter("decoder", KotlinxSerialization.Decoder) - .returns(NOTHING) - .addStatement("throw %T()", Stdlib.AssertionError) - .build(), - ) - .addFunction( - FunSpec.builder("encode") - .addParameter("json", KotlinxSerialization.Json) - .addParameter("value", modifierType) - .returns(Protocol.ModifierElement) - .addStatement("val element = json.encodeToJsonElement(this, value)") - .addStatement( - "return %T(%T(%L), element)", - Protocol.ModifierElement, - Protocol.ModifierTag, - modifier.tag, - ) - .build(), + .build() + } else { + null + } + + addProperty( + PropertySpec.builder( + name = serializerTagAndSerializer.simpleName, + type = Stdlib.Pair.parameterizedBy( + Protocol.ModifierTag, + serializationStrategyType.copy(nullable = true), + ), + INTERNAL, + ) + .initializer( + "%T(%L) to %L", + Protocol.ModifierTag, + modifier.tag, + serializationStrategyValue, ) .build(), ) @@ -689,66 +691,53 @@ internal fun generateProtocolModifierSerializers( } } -internal fun generateComposeProtocolModifierSerialization( +/* +@RedwoodCodegenApi +@Suppress("UNCHECKED_CAST") +public override fun modifierTagAndSerializationStrategy( + element: T, +): Pair?> = + when (element) { + is CustomType -> CustomTypeTagAndSerializer + is CustomTypeStateless -> CustomTypeStatelessTagAndSerializer + else -> throw AssertionError() + } +*/ +internal fun modifierTagAndSerializationStrategy( schemaSet: ProtocolSchemaSet, -): FileSpec { +): FunSpec { val schema = schemaSet.schema - val name = schema.modifierToProtocol.simpleName - return buildFileSpec(schema.guestProtocolPackage(), "modifierSerialization") { - addAnnotation(suppressDeprecations) - addFunction( - FunSpec.builder(name) - .addModifiers(INTERNAL) - .receiver(Redwood.Modifier) - .addParameter("json", KotlinxSerialization.Json) - .returns(LIST.parameterizedBy(Protocol.ModifierElement)) - .beginControlFlow("return %M", Stdlib.buildList) - .addStatement( - "this@%L.forEach { element -> add(element.%M(json)) }", - name, - schema.modifierToProtocol, - ) - .endControlFlow() + val allModifiers = schemaSet.allModifiers() + val t = TypeVariableName("T", Redwood.ModifierElement) + val returnType = Stdlib.Pair.parameterizedBy( + Protocol.ModifierTag, + KotlinxSerialization.KSerializer.parameterizedBy(t).copy(nullable = true), + ) + return FunSpec.builder("modifierTagAndSerializationStrategy") + .addAnnotation(Redwood.RedwoodCodegenApi) + .addAnnotation( + AnnotationSpec.builder(Suppress::class) + .addMember("%S", "UNCHECKED_CAST") .build(), ) - addFunction( - FunSpec.builder(schema.modifierToProtocol) - .addModifiers(PRIVATE) - .receiver(Redwood.ModifierElement) - .addParameter("json", KotlinxSerialization.Json) - .returns(Protocol.ModifierElement) - .beginControlFlow("return when (this)") - .apply { - val modifier = schemaSet.allModifiers() - if (modifier.isEmpty()) { - addAnnotation( - AnnotationSpec.builder(Suppress::class) - .addMember("%S, %S", "UNUSED_PARAMETER", "UNUSED_EXPRESSION") - .build(), - ) - } else { - for ((localSchema, modifier) in modifier) { - val modifierType = localSchema.modifierType(modifier) - val surrogate = localSchema.modifierSerializer(modifier, schema) - if (modifier.properties.isEmpty()) { - addStatement( - "is %T -> %T(%T(%L))", - modifierType, - Protocol.ModifierElement, - Protocol.ModifierTag, - modifier.tag, - ) - } else { - addStatement("is %T -> %T.encode(json, this)", modifierType, surrogate) - } - } - } - } - .addStatement("else -> throw %T()", Stdlib.AssertionError) - .endControlFlow() - .build(), - ) - } + .addModifiers(PUBLIC, OVERRIDE) + .addTypeVariable(t) + .addParameter("element", t) + .returns(returnType) + .addCode("return when·(element)·{⇥\n") + .apply { + for ((localSchema, modifier) in allModifiers) { + val modifierType = localSchema.modifierType(modifier) + addStatement( + "is %T -> %M", + modifierType, + localSchema.modifierTagAndSerializer(modifier, schema), + ) + } + } + .addStatement("else -> throw %T()", Stdlib.AssertionError) + .addCode("⇤} as %T\n", returnType) + .build() } private fun Schema.protocolWidgetFactoryType(host: Schema): ClassName { @@ -759,9 +748,6 @@ private fun Schema.protocolWidgetType(widget: Widget, host: Schema): ClassName { return ClassName(guestProtocolPackage(host), "Protocol${widget.type.flatName}") } -private fun Schema.modifierSerializer(modifier: Modifier, host: Schema): ClassName { - return ClassName(guestProtocolPackage(host), modifier.type.flatName + "Serializer") +private fun Schema.modifierTagAndSerializer(modifier: Modifier, host: Schema): MemberName { + return MemberName(guestProtocolPackage(host), modifier.type.flatName + "TagAndSerializer") } - -internal val Schema.modifierToProtocol: MemberName get() = - MemberName(guestProtocolPackage(), "toProtocol") diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt index 56305e7104..b356339a2a 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt @@ -112,7 +112,7 @@ internal object Stdlib { val ExperimentalObjCName = ClassName("kotlin.experimental", "ExperimentalObjCName") val List = ClassName("kotlin.collections", "List") val ObjCName = ClassName("kotlin.native", "ObjCName") - val buildList = MemberName("kotlin.collections", "buildList") + val Pair = ClassName("kotlin", "Pair") val listOf = MemberName("kotlin.collections", "listOf") } @@ -124,6 +124,7 @@ internal object KotlinxSerialization { val ExperimentalSerializationApi = ClassName("kotlinx.serialization", "ExperimentalSerializationApi") val KSerializer = ClassName("kotlinx.serialization", "KSerializer") val Serializable = ClassName("kotlinx.serialization", "Serializable") + val SerializationStrategy = ClassName("kotlinx.serialization", "SerializationStrategy") val serializer = MemberName("kotlinx.serialization", "serializer") val SerialDescriptor = ClassName("kotlinx.serialization.descriptors", "SerialDescriptor") diff --git a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt index ba70b500ef..a5b6e9ff04 100644 --- a/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt +++ b/redwood-tooling-codegen/src/test/kotlin/app/cash/redwood/tooling/codegen/ProtocolGuestGenerationTest.kt @@ -18,12 +18,9 @@ package app.cash.redwood.tooling.codegen import app.cash.redwood.schema.Property import app.cash.redwood.schema.Schema import app.cash.redwood.schema.Widget -import app.cash.redwood.tooling.schema.ProtocolSchemaSet import app.cash.redwood.tooling.schema.parseTestSchema -import assertk.all import assertk.assertThat import assertk.assertions.contains -import com.example.redwood.testapp.TestSchema import org.junit.Test class ProtocolGuestGenerationTest { @@ -51,14 +48,4 @@ class ProtocolGuestGenerationTest { """.trimMargin(), ) } - - @Test fun `dependency layout modifier are included in serialization`() { - val schemaSet = ProtocolSchemaSet.load(TestSchema::class) - - val fileSpec = generateComposeProtocolModifierSerialization(schemaSet) - assertThat(fileSpec.toString()).all { - contains("is TestRowVerticalAlignment -> TestRowVerticalAlignmentSerializer.encode(json, this)") - contains("is Grow -> GrowSerializer.encode(json, this)") - } - } } diff --git a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt index f244724654..8961fcea84 100644 --- a/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt +++ b/redwood-treehouse-guest/src/jsMain/kotlin/app/cash/redwood/treehouse/ProtocolBridgeJs.kt @@ -15,13 +15,13 @@ */ package app.cash.redwood.treehouse +import app.cash.redwood.Modifier import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.protocol.Change import app.cash.redwood.protocol.ChangesSink import app.cash.redwood.protocol.ChildrenTag import app.cash.redwood.protocol.Event import app.cash.redwood.protocol.Id -import app.cash.redwood.protocol.ModifierElement import app.cash.redwood.protocol.PropertyTag import app.cash.redwood.protocol.RedwoodVersion import app.cash.redwood.protocol.WidgetTag @@ -49,7 +49,7 @@ internal actual fun GuestProtocolAdapter( internal class FastGuestProtocolAdapter( override val json: Json = Json.Default, hostVersion: RedwoodVersion, - widgetSystemFactory: ProtocolWidgetSystemFactory, + private val widgetSystemFactory: ProtocolWidgetSystemFactory, private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing, ) : GuestProtocolAdapter { private var nextValue = Id.Root.value + 1 @@ -138,12 +138,23 @@ internal class FastGuestProtocolAdapter( changes.push(js("""["property",{"id":id,"tag":tag,"value":value}]""")) } - override fun appendModifierChange( - id: Id, - elements: List, - ) { + override fun appendModifierChange(id: Id, value: Modifier) { + val elements = js("[]") + + value.forEach { element -> + val (tag, serializer) = widgetSystemFactory.modifierTagAndSerializationStrategy(element) + when { + serializer != null -> { + val value = json.encodeToDynamic(serializer, element) + elements.push(js("""[tag,value]""")) + } + else -> { + elements.push(js("""[tag]""")) + } + } + } + val id = id - val elements = Json.encodeToDynamic(elements) changes.push(js("""["modifier",{"id":id,"elements":elements}]""")) } diff --git a/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt index 0c568ba4b0..6c7cf955e4 100644 --- a/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt +++ b/redwood-treehouse-guest/src/jsTest/kotlin/app/cash/redwood/treehouse/FastGuestProtocolAdapterTest.kt @@ -24,11 +24,13 @@ import app.cash.redwood.protocol.guest.guestRedwoodVersion import app.cash.redwood.widget.Widget import assertk.assertThat import assertk.assertions.isEqualTo +import com.example.redwood.testapp.compose.TestScope import com.example.redwood.testapp.compose.backgroundColor import com.example.redwood.testapp.protocol.guest.TestSchemaProtocolWidgetSystemFactory import com.example.redwood.testapp.widget.TestSchemaWidgetSystem import kotlin.test.Test import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.serializer @@ -62,6 +64,20 @@ class FastGuestProtocolAdapterTest { } } + @Test fun consistentWithDefaultGuestProtocolAdapterForModifiers() { + assertChangesEqual { root, widgetSystem -> + with(object : TestScope {}) { + val button = widgetSystem.TestSchema.Button() + button.modifier = Modifier + .backgroundColor(0xff0000u) + .customType(5.seconds) + .customTypeWithDefault(10.seconds, "sup") + .customTypeStateless() + root.insert(0, button) + } + } + } + /** Test our special case for https://github.com/Kotlin/kotlinx.serialization/issues/2713 */ @Test fun consistentWithDefaultGuestProtocolAdapterForUint() { assertChangesEqual { root, widgetSystem ->