-
Notifications
You must be signed in to change notification settings - Fork 28
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
refactor: decrease generated artifact size #1057
Conversation
@InternalApi | ||
public sealed interface HttpSerializer<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: What benefit does nesting the variants of this interface provide? There are no common members and no way they could be used interchangeably...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
namespacing, HttpSerializer.Streaming
vs HttpStreamingSerializer
(or HttpSerializerStreaming
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's solely namespacing, it seems clearer to use object
. Seeing a common parent HttpSerializer<T>
interface in a few places tripped me up when first reviewing this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no common members and no way they could be used interchangeably...
I'm not sure what this has to do with whether to nest variants inside the body of the sealed interface or not. It's not solely namespacing as you still need the parent sealed interface.
/** | ||
* Serializer for streaming operations that need full control over serialization of the body | ||
*/ | ||
@InternalApi | ||
public interface Streaming<T> : HttpSerializer<T> { | ||
public suspend fun serialize(context: ExecutionContext, input: T): HttpRequestBuilder | ||
} | ||
|
||
/** | ||
* Serializer for non-streaming (simple) operations that don't need to ever suspend. | ||
*/ | ||
@InternalApi | ||
public interface NonStreaming<T> : HttpSerializer<T> { | ||
public fun serialize(context: ExecutionContext, input: T): HttpRequestBuilder | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Could these be made fun interface
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They could be but I think it was perhaps a mistake originally to do so as we don't know if we'll want to ever expand the interface in some way in the future which we cannot do with a functional interface.
@Suppress("DEPRECATION") | ||
internal constructor( | ||
execution: SdkOperationExecution<I, O>, | ||
context: ExecutionContext, | ||
serializer: HttpSerialize<I>, | ||
deserializer: HttpDeserialize<O>, | ||
typeInfo: OperationTypeInfo, | ||
telemetry: SdkOperationTelemetry, | ||
) : this(execution, context, serializer.intoSerializer(), deserializer.intoDeserializer(), typeInfo, telemetry) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Mark as @Deprecated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's needed since HttpSerialize<T>
and HttpDeserialize<T>
are deprecated which will trigger a deprecation warning anyway.
@Suppress("DEPRECATION") | ||
private fun buildOperation( | ||
requestBody: String = "{\"TableName\": \"foo\"}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: In general, this PR suppresses a lot of now-deprecated patterns in existing tests. Shouldn't we update those tests to use the modern methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated many of them but some of them. use UnitSerializer
or IdentitySerializer
and I didn't feel like re-writing them. The changes we made here are an optimization, there isn't anything wrong with the old interfaces other than the way they are used in codegen introduces more suspend paths than is actually necessary.
@@ -77,7 +77,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { | |||
* The function should have the following signature: | |||
* | |||
* ``` | |||
* suspend fun throwFooOperationError(context: ExecutionContext, call: HttpCall): Nothing { | |||
* fun throwFooOperationError(context: ExecutionContext, call: HttpCall, payload: HttpByteArray?): Nothing { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: HttpByteArray
-> ByteArray
val requestBuilder = when (serializer) { | ||
is HttpSerializer.NonStreaming -> serializer.serialize(modified.context, modified.subject) | ||
is HttpSerializer.Streaming -> serializer.serialize(modified.context, modified.subject) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are doing the same thing, is the when
block necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, there is no common method between the two you have to "unwrap" the actual type by matching on it in it with when
.
@InternalApi | ||
public sealed interface HttpSerializer<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's solely namespacing, it seems clearer to use object
. Seeing a common parent HttpSerializer<T>
interface in a few places tripped me up when first reviewing this PR.
Issue #
see awslabs/aws-sdk-kotlin#411
Description of changes
This PR attempts to decrease the generated artifact size of service clients by doing the following:
suspend
from most HTTP operation serializers and deserializersThe changes and results are detailed in the sections below for each of these.
Inline higher order functions
You might consider this a bug since it was introduced with a refactor but in any case we have a lot of generated code
in serializers and deserializers that looks something like:
All of the invocations like
builder.url {...}
,builder.headers {...}
,parameters.decodedParameters{...}
, etc takea lambda argument. This results in a lot of backing classes to hold the captured state (e.g.
input
) from the outer context.main
with inlining
DELTA AFTER INLININING
Remove most suspend points for generated HttpSerde
The only serializers and deserializers that suspend are the ones that deal with streaming types but we generate all operation serializers and deserializers as if they will
suspend
. Deserializers that just read the payload onlysuspend
to pull the payload into memory to invoke the format (e.g. JSON, XML, etc) deserializer on it. This suspension point can be lifted into the runtime by providing separate interfaces forsuspend
and non.DELTA FROM INLINING
Totals after inlining + http serde changes
Total delta with both inlining and HTTP serde changes compared to original (JVM) artifact sizes
Appendix
The extracted artifacts before and after changes:
Latest S3 JVM jar:
After inlining + HTTP serde
For comparison with Java v2 SDK:
Java S3 latest:
Java DDB latest:
Next Steps
SdkSerializable
As noted in awslabs/aws-sdk-kotlin#411 (comment) the way we generate nested struct/union serialization causes backing classes to be generated to hold the required state. I looked for ways to remove this but none are easy/clean. The best solution here is to revisit serialization and make it format specific like we did for XML deserialization . This would remove quite a bit of size from artifacts I'd imagine as we have a lot of these in practice.
All of the
field(<DESCRIPTOR>, T, ::serializeFoo)
calls andserializeSdkSerializable(...)
calls generate an additional backing class.Reduce operation error handling overhead
throwFooOperationError
is a top level function that gets generated into a separate.class
file. Class fileshave an overhead though so it may be smaller to just encode this into the operation error deserializer interface so they share the same class file OR for AWS protocols at least we could combine all operation handlers into a single function like
throwS3Error(...)
. This should work because AWS protocols all have the type of the error in the response and so having lots of separate functions is unnecessary. They would behave the same if combined into one.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.