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

Optimize Modifier serialization in guest code #2253

Merged
merged 8 commits into from
Aug 26, 2024

Conversation

swankjesse
Copy link
Collaborator

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.


  • CHANGELOG.md's "Unreleased" section has been updated, if applicable.


public interface ProtocolWidgetSystemFactory {
/** Create a new [WidgetSystem] connected to a host via [guestAdapter]. */
public fun create(
guestAdapter: GuestProtocolAdapter,
mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
): WidgetSystem<Unit>

@RedwoodCodegenApi
public fun modifierTag(element: Modifier.Element): ModifierTag
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Tempted to do one function that returns a Pair<ModifierTag, KSerializer<T>?> instead of two functions. The only hazard of doing one function is I’d like to avoid allocating a Pair if possible.

Copy link
Member

Choose a reason for hiding this comment

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

We generate the KSerializer for every modifier. We can do two things:

  1. Switch to subtyping SerializationStrategy in the generated type rather than KSerializer. We generate custom serializers because we only want the serialization code, so we should only be implementing the serialization interface (no clue why I didn't do this initially).
  2. Define a ProtocolSerializationStrategy interface which adds val tag: ModifierTag to SerializationStrategy and switch our codegen to use that. This eliminates the need to do a double, type-based lookup.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like both of these ideas. I’ll add a commit to this PR to do that!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. Unfortunately I couldn’t do it exactly as suggested 'cause we don’t have a serializer for stateless modifiers. So instead it’s a Pair<ModifierTag, SerializationStrategy>. Seems to work okay and read okay.


/** Returns null if the modifier is stateless and should serialize as null. */
@RedwoodCodegenApi
public fun <T : Modifier.Element> modifierSerializer(element: T): KSerializer<T>?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On my first attempt this returned a non-null KSerializer<T>, but I ran into a this bug in Kotlinx.serialization. I’d like to contribute a fix for that bug and then later I can change this to return non-null always.

import app.cash.redwood.widget.WidgetSystem
import kotlinx.serialization.KSerializer

public interface ProtocolWidgetSystemFactory {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With these two new APIs I don’t think the Factory name fit as well as it did previously.

serializer == null -> null
else -> json.encodeToDynamic(serializer, element)
}
elements.push(js("""[tag,value]"""))
Copy link
Member

Choose a reason for hiding this comment

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

The protocol when used normally does not encode null values here. There's no real harm in it, though, except for the 5 extra bytes on the wire.

Copy link
Member

Choose a reason for hiding this comment

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

We might need to move our protocol serialization tests upward to ensure these custom forms mirror it in behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ooooh good catch, I missed that. I think it’s a nice improvement to test at the JSON-encoded layer for this, though I don’t think Zipline’s asDynamicFunction() makes this particularly easy. It’s trying to hide the JSON layer and the test wants to exercise it!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed to not encode null, but I haven’t yet fixed the test case to validate the JSON matches as specified. I’ll need to contemplate how to do that in a way that feels right at this layer.

@swankjesse swankjesse enabled auto-merge (squash) August 26, 2024 19:33
@swankjesse swankjesse force-pushed the jwilson.0821.modifier_serialization branch from 735934d to bec3c19 Compare August 26, 2024 21:48
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.
@swankjesse swankjesse force-pushed the jwilson.0821.modifier_serialization branch from bec3c19 to cedf07f Compare August 26, 2024 21:48
@swankjesse swankjesse merged commit 1010184 into trunk Aug 26, 2024
11 checks passed
@swankjesse swankjesse deleted the jwilson.0821.modifier_serialization branch August 26, 2024 22:34
This pull request was closed.
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.

2 participants