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

Remove kotlin-reflect, replace with kotlinx-metadata-jvm #1076

Closed
wants to merge 22 commits into from

Conversation

ZacSweers
Copy link
Collaborator

@ZacSweers ZacSweers commented Jan 15, 2020

Kotlin-reflect has been a long-standing thorn for KotlinJsonAdapter for a number of reasons:

  • It has a huge impact on binary size, both in terms of the library and the compiler impact. When removing it from the slack code base recently, it saved us nearly 10k methods just by removing it (even though only 2 were used!)
  • It's super slow and heavyweight
  • It has a number of tricky proguard considerations

This PR replaces kotlin-reflect entirely with kotlinx-metadata-reflect, the low-level metadata library we use in code gen (via kotlinpoet-metadata). It's also what the kotlinx.reflect.lite project is in the process of being migrated to, and used in a number of other major toolchains now (Dagger, R8, etc).

Effectively, this skips past kotlinx.reflect.lite and replaces kotlin-reflect with something closer to an "ultralight" setup, set up specific to Moshi's needs. It's a significant change to the adapter, but the core logic remains the same with one notable exception (documented at the end).

This supersedes #307, but effectively resolves #307 in spirit.

Stats

Quick androidx-benchmark on my serialization benchmarks project shows this is ~21.5% faster for buffer reads as well! ZacSweers/json-serialization-benchmarking#10

AndroidBenchmark.moshi_kotlin_codegen_buffer_toJson[minified=true]                   5,052,032  ns
AndroidBenchmark.moshi_kotlin_codegen_string_toJson[minified=true]                   7,072,449  ns
AndroidBenchmark.moshi_kotlin_reflective_metadata_buffer_toJson[minified=true]       8,096,823  ns
AndroidBenchmark.moshi_kotlin_reflective_buffer_toJson[minified=true]                9,461,355  ns
AndroidBenchmark.moshi_kotlin_reflective_string_toJson[minified=true]               11,308,542  ns

AndroidBenchmark.moshi_kotlin_codegen_buffer_fromJson[minified=true]                 7,553,282  ns
AndroidBenchmark.moshi_kotlin_codegen_string_fromJson[minified=true]                 8,746,668  ns
AndroidBenchmark.moshi_kotlin_reflective_metadata_buffer_fromJson[minified=true]     9,167,501  ns
AndroidBenchmark.moshi_kotlin_codegen_buffer_fromJson[minified=false]               10,039,950  ns
AndroidBenchmark.moshi_kotlin_reflective_buffer_fromJson[minified=true]             10,454,168  ns
AndroidBenchmark.moshi_kotlin_reflective_metadata_buffer_fromJson[minified=false]   11,691,721  ns
AndroidBenchmark.moshi_kotlin_codegen_string_fromJson[minified=false]               11,716,408  ns
AndroidBenchmark.moshi_kotlin_reflective_string_fromJson[minified=true]             11,734,532  ns
AndroidBenchmark.moshi_kotlin_reflective_buffer_fromJson[minified=false]            12,930,158  ns
AndroidBenchmark.moshi_kotlin_reflective_string_fromJson[minified=false]            14,747,397  ns

Two open questions

  • The behavior change is that moshi-kotlin still allowed extending from and encoding non-kotlin superclasses and their properties. I think this was an oversight when we stopped supporting mixing them in Moshi 1.8, as this doesn't match the behavior of code gen.
  • What are the appropriate proguard rules for this?

@ZacSweers ZacSweers changed the title Remove kotlin-reflect, replace with kotlinx-metadata-reflect Remove kotlin-reflect, replace with kotlinx-metadata-jvm Jan 15, 2020
for (property in allPropertiesSequence.distinctBy { it.name }) {
val propertyField = property.fieldSignature?.let { signature ->
val signatureString = signature.asString()
rawType.declaredFields.find { it.jvmFieldSignature == signatureString }
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 think I need to do an allFields() style function for this as well to be safe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

return classMetadata.toKmClass()
}

private fun Class<*>.allMethods(): Sequence<Method> {
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 wonder if this is worth sticking a lazily-filled cache in front of

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

@rharter rharter left a comment

Choose a reason for hiding this comment

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

This all sounds good to me. I haven't used these libraries extensively so I'm not sure I have the expertise to thoroughly evaluate the implementation, but don't see any red flags on review.

get() {
return buildString {
append('(')
parameterTypes.joinTo(this, separator = "", transform = { it.descriptor })
Copy link

Choose a reason for hiding this comment

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

Minor, but you can use one-liners for this

internal val Constructor.descriptor: String
    get() = parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")V") { it.descriptor }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call! 6cff563

return when {
this === other -> true
this != null && other != null -> {
abbreviatedType == other.abbreviatedType
Copy link

Choose a reason for hiding this comment

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

Note that this will imply that types which are represented with the same runtime type are different, if they have a different abbreviation. Not sure if you'd like this, but if you would, then never mind:

typealias ListOfString = List<String>
typealias MyList<T> = List<T>

KmType of ListOfString isEqualTo KmType of MyList<String> -> false

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, that's a good point. Since we're just doing this for checking equality of the parameter and property. I guess since it's a legal assignment we can drop the alias check. Done in 36f945b

variance == other.variance
&& type isEqualTo other.type
}
this == null && other == null -> true
Copy link

Choose a reason for hiding this comment

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

I suppose this is already checked by this === other -> true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, good point! I added the this === other check later 🙃 d3e6e36

val annotations: List<Annotation>
) {
val name get() = km.name
val isOptional get() = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(km.flags)
Copy link

Choose a reason for hiding this comment

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

Note that semantics of "is optional" are bit different from "declares default value". Not sure how important it is for your use case:

interface A {
    fun foo(x: String = "...")
}

interface B : A {
    override fun foo(x: String)
}

x in B.foo is optional, although it does not declare default value.

KParameter.isOptional implements the "is optional" semantics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Interesting, that's a good point. I think we can stick with just our default check since this is only ever checked on constructor parameters. Updated the naming of this though to reflect the default value semantic though! 164bd08

@ZacSweers
Copy link
Collaborator Author

Updated with modernized proguard rules for this approach that also leverage R8's features where appropriate (based off of this PR for kotlin-reflect rules JetBrains/kotlin#2893)

@swankjesse want to get your sign-off before merging, especially with the platform types behavior change.

Comment on lines 11 to 19
# Keep names of Kotlin classes but allow shrinking if unused
-keepnames @kotlin.Metadata class *

# Keep fields, constructors, and methods in Kotlin classes.
-keepclassmembers @kotlin.Metadata class * {
<fields>;
<init>(...);
<methods>;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@udalov @jsjeon I wonder if these should be added to the kotlin-reflect rules as well

Copy link

Choose a reason for hiding this comment

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

For some reason I thought that the -keep class kotlin.Metadata rule would also keep all members. If that's not true, then your proposal seems reasonable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is slightly different, and rather it applies this rule only to classes annotated with Metadata, not the metadata annotation class itself

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thinking on it more, I think it's probably best to omit these for now and have the guidance be that users apply keep rules as needed for their exact cases. Same with kotlin-reflect - the rules in place now make sure its machinery works, and users can just make sure they handle appropriate rules for classes that they plan to reflect on

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cleaned up in ad47a56

Copy link

Choose a reason for hiding this comment

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

Oops, I should have paid attention to github alarms more often and carefully. Sorry for the late game.

@udalov

I thought that the -keep class kotlin.Metadata rule would also keep all members.

That's not true. The rule doesn't specify members, and thus all members are still eligible for shrinking and minification. Since members, such as k, d1, etc., are read all together (via metadata library), the correct rule is -keep class kotlin.Metadata { *; }.

But, as @ZacSweers already mentioned, rules here point to classes with metadata annotation, not the metadata definition itself.

Regarding having similar rules in kotlin-reflect, I would say no, because it's still up to users to add such rules if some classes (w/ or w/o metadata annotation) have reflective uses. Glad to see that you ended up with the same conclusion.

@ZacSweers
Copy link
Collaborator Author

Just a note for posterity - @swankjesse requested this not be merged until he can review as well

@ZacSweers ZacSweers closed this Mar 28, 2020
@ZacSweers ZacSweers reopened this Jul 29, 2020
@ZacSweers
Copy link
Collaborator Author

Will reopen a new PR, I can't push to this branch anymore since I stepped back from the project 😬

@ZacSweers
Copy link
Collaborator Author

Continued here #1183

@oldergod oldergod deleted the z/metadataReflect branch August 5, 2020 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Switch to kotlinx.reflect.lite when it’s sufficient
4 participants