Skip to content

Commit 649ba85

Browse files
authored
Optimize interceptor code paths (#4597)
## Motivation and Context Addresses #4114 Every SDK operation invokes ~16 interceptor hooks via dyn dispatch. Most interceptors only override 1–2 hooks, meaning the majority of calls dispatch into no-op defaults. Additionally, every interceptor checks `DisableInterceptor<T>` in the config bag on every invocation, even for SDK-internal interceptors that are never disabled. ### Benchmark summary The existing [`previous-release-comparison`](https://github.com/smithy-lang/smithy-rs/tree/main/aws/sdk/benchmarks/previous-release-comparison) benchmark crate (updated in this PR) was used to measure the impact of each change. S3 `ListObjectsV2` with mock HTTP client (no network). The S3 crate was generated locally with `endpointRuleSet` trait (not BDD) for apples-to-apples comparison against the published release. | Step | Commit | Description | Avg | Δ from prev step | Cumulative Δ | |------|--------|-------------|-----|-------------------|--------------| | Baseline | — | Previous release (1.129.0) | 66.64 µs | — | — | | 1 | [`7d8a725`](7d8a725) | Skip disable check for permanent interceptors | 55.22 µs ±0.50 | −11.42 µs (−17.1%) | −11.42 µs (−17.1%) | | 2 | [`cb490c0`](cb490c0) | Skip dyn dispatch for unoverridden hooks | 53.46 µs ±0.52 | −1.76 µs (−3.2%) | −13.18 µs (−19.8%) | | 3 | [`62a725b`](62a725b) | Reuse client builder for merged RuntimeComponents | 50.79 µs ±0.12 | −2.67 µs (−5.0%) | −15.85 µs (−23.8%) | The description below breaks down each optimization in the order listed above. ## Description ### Skip disable check for permanent interceptors (−17.1%) Introduces `SharedInterceptor::permanent()`, which skips the per-invocation `DisableInterceptor<T>` config bag lookup. SDK-internal interceptors that are never disabled (content length enforcement, metrics, token bucket, checksum, idempotency token, request compression, feature trackers) are now registered via this path. Interceptors that *can* be disabled — `InvocationIdInterceptor`, `UserAgentInterceptor`, `RequestInfoInterceptor` (disabled during presigning) — remain registered via `SharedInterceptor::new()`. In debug builds, a `debug_assert!` fires if someone attempts to call `disable_interceptor` on a permanent interceptor, catching the misconfiguration early with a clear error message. This uses a type-erased `fn(&ConfigBag) -> bool` pointer captured at construction time (when the concrete type `T` is still known), gated behind `#[cfg(debug_assertions)]` so there is zero cost in release builds. ### Skip dyn dispatch for unoverridden hooks (−3.2%) Adds a new proc-macro crate `aws-smithy-runtime-api-macros` with a `#[dyn_dispatch_hint]` attribute macro. Placed on an `impl Intercept` block, it inspects which hook methods are overridden and auto-generates an `overridden_hooks()` method returning the correct `OverriddenHooks` bitmask. The interceptor dispatch loop checks this bitmask before calling each hook, skipping dyn dispatch into no-op defaults. A new crate was necessary because manually specifying bitflags at the `SharedInterceptor` construction site would be error-prone and drift over time. For example, without the macro, callers would need to write something like: ```rust SharedInterceptor::permanent(MyInterceptor::new()) .with_override_hint(OverriddenHooks::MODIFY_BEFORE_SIGNING | OverriddenHooks::MODIFY_BEFORE_TRANSMIT) ``` This creates a maintenance hazard: if someone adds or removes a hook override in the `impl Intercept` block but forgets to update the construction site, the flags silently go out of sync. The macro derives the flags directly from the impl block, so they can never drift. Interceptors not annotated with `#[dyn_dispatch_hint]` default to `overridden_hooks() -> all()`, preserving existing behavior. All internal interceptors are annotated with `#[dyn_dispatch_hint]`. The trait method, `OverriddenHooks` type, and macro re-export are all `#[doc(hidden)]`. ### Reuse client builder for merged RuntimeComponents (−5.0%) Eliminates an extra `RuntimeComponents::builder()` + `merge_from` in `apply_configuration` by merging the operation builder directly into the existing client builder. ## Testing - CI - New integration tests in `aws-smithy-runtime-api/tests/permanent_interceptor.rs`: permanent vs disableable behavior, debug assertion on misuse - New integration tests in `aws-smithy-runtime-api/tests/dyn_dispatch_hint.rs`: macro generates correct `OverriddenHooks` flags for single hook, multiple hooks, no hooks, and without-macro cases ### Compile time impact The new proc-macro crate (`aws-smithy-runtime-api-macros`) adds `syn`, `quote`, and `proc-macro2` as dependencies. No measurable compile time regression on `aws-sdk-s3` release builds (hyperfine, 10 runs with `cargo clean` between each on `m7i.xlarge`): | Branch | Mean | ±σ | Range | Instance | |--------|------|----|-------|----------| | `main` | 125.367s | ±2.538s | 123.2–132.0s | m7i.xlarge, Amazon Linux 2023 | | This PR | 124.770s | ±1.216s | 122.3–127.0s | m7i.xlarge, Amazon Linux 2023 |
1 parent 3b65879 commit 649ba85

69 files changed

Lines changed: 1467 additions & 615 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AccountIdEndpointParamsDecorator.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,12 @@ class AccountIdEndpointModeBuiltInParamDecorator : ConditionalDecorator(
253253

254254
private class AccountIdEndpointFeatureTrackerInterceptor(codegenContext: ClientCodegenContext) :
255255
ServiceRuntimePluginCustomization() {
256+
private val runtimeConfig = codegenContext.runtimeConfig
257+
256258
override fun section(section: ServiceRuntimePluginSection) =
257259
writable {
258260
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
259-
section.registerInterceptor(this) {
261+
section.registerPermanentInterceptor(runtimeConfig, this) {
260262
rustTemplate(
261263
"#{Interceptor}",
262264
"Interceptor" to

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/AwsChunkedContentEncodingDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private class AwsChunkedOperationCustomization(
159159
is OperationSection.AdditionalInterceptors -> {
160160
if (!operationRequiresAwsChunked(codegenContext, operation)) return@writable
161161

162-
section.registerInterceptor(runtimeConfig, this) {
162+
section.registerPermanentInterceptor(runtimeConfig, this) {
163163
rustTemplate(
164164
"""
165165
#{AwsChunkedContentEncodingInterceptor}

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/EndpointOverrideMetricDecorator.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class EndpointOverrideMetricDecorator : ClientCodegenDecorator {
4141
##[derive(Debug, Default)]
4242
pub(crate) struct EndpointOverrideFeatureTrackerInterceptor;
4343
44+
##[#{dyn_dispatch_hint}]
4445
impl #{Intercept} for EndpointOverrideFeatureTrackerInterceptor {
4546
fn name(&self) -> &'static str {
4647
"EndpointOverrideFeatureTrackerInterceptor"
@@ -60,6 +61,7 @@ class EndpointOverrideMetricDecorator : ClientCodegenDecorator {
6061
}
6162
""",
6263
"Intercept" to smithyRuntimeApi.resolve("client::interceptors::Intercept"),
64+
"dyn_dispatch_hint" to smithyRuntimeApi.resolve("client::interceptors::dyn_dispatch_hint"),
6365
"BeforeSerializationInterceptorContextRef" to
6466
smithyRuntimeApi.resolve("client::interceptors::context::BeforeSerializationInterceptorContextRef"),
6567
"ConfigBag" to smithyTypes.resolve("config_bag::ConfigBag"),
@@ -84,7 +86,7 @@ private class EndpointOverrideFeatureTrackerRegistration(
8486
override fun section(section: ServiceRuntimePluginSection) =
8587
writable {
8688
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
87-
section.registerInterceptor(this) {
89+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
8890
rustTemplate("crate::config::endpoint::EndpointOverrideFeatureTrackerInterceptor")
8991
}
9092
}

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class HttpRequestChecksumCustomization(
174174
when (section) {
175175
is OperationSection.AdditionalInterceptors -> {
176176
if (requestAlgorithmMemberName != null) {
177-
section.registerInterceptor(runtimeConfig, this) {
177+
section.registerPermanentInterceptor(runtimeConfig, this) {
178178
val runtimeApi = RuntimeType.smithyRuntimeApiClient(runtimeConfig)
179179
rustTemplate(
180180
"""

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/HttpResponseChecksumDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class HttpResponseChecksumCustomization(
131131

132132
when (section) {
133133
is OperationSection.AdditionalInterceptors -> {
134-
section.registerInterceptor(codegenContext.runtimeConfig, this) {
134+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
135135
// CRC32, CRC32C, SHA256, SHA1 -> "crc32", "crc32c", "sha256", "sha1"
136136
val responseAlgorithms =
137137
checksumTrait.responseAlgorithms

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/ObservabilityMetricDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private class ObservabilityFeatureTrackerInterceptor(private val codegenContext:
3434
override fun section(section: ServiceRuntimePluginSection) =
3535
writable {
3636
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
37-
section.registerInterceptor(this) {
37+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
3838
val runtimeConfig = codegenContext.runtimeConfig
3939
rustTemplate(
4040
"#{Interceptor}",

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/RecursionDetectionDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private class RecursionDetectionRuntimePluginCustomization(
3030
override fun section(section: ServiceRuntimePluginSection): Writable =
3131
writable {
3232
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
33-
section.registerInterceptor(this) {
33+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
3434
rust(
3535
"#T::new()",
3636
AwsRuntimeType.awsRuntime(codegenContext.runtimeConfig)

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/RetryInformationHeaderDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ private class AddRetryInformationHeaderInterceptors(codegenContext: ClientCodege
3333
writable {
3434
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
3535
// Track the latency between client and server.
36-
section.registerInterceptor(this) {
36+
section.registerPermanentInterceptor(runtimeConfig, this) {
3737
rust(
3838
"#T::new()",
3939
awsRuntime.resolve("service_clock_skew::ServiceClockSkewInterceptor"),

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/customize/apigateway/ApiGatewayDecorator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private class ApiGatewayAcceptHeaderInterceptorCustomization(private val codegen
3232
override fun section(section: ServiceRuntimePluginSection): Writable =
3333
writable {
3434
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
35-
section.registerInterceptor(this) {
35+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
3636
rustTemplate(
3737
"#{Interceptor}::default()",
3838
"Interceptor" to

aws/codegen-aws-sdk/src/main/kotlin/software/amazon/smithy/rustsdk/customize/glacier/GlacierDecorator.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private class GlacierApiVersionCustomization(private val codegenContext: ClientC
8989
writable {
9090
if (section is ServiceRuntimePluginSection.RegisterRuntimeComponents) {
9191
val apiVersion = codegenContext.serviceShape.version
92-
section.registerInterceptor(this) {
92+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
9393
rustTemplate(
9494
"#{Interceptor}::new(${apiVersion.dq()})",
9595
"Interceptor" to inlineModule(codegenContext.runtimeConfig).resolve("GlacierApiVersionInterceptor"),
@@ -113,7 +113,7 @@ private class GlacierOperationInterceptorsCustomization(private val codegenConte
113113
val inputShape = codegenContext.model.expectShape(section.operationShape.inputShape) as StructureShape
114114
val inlineModule = inlineModule(codegenContext.runtimeConfig)
115115
if (inputShape.inputWithAccountId()) {
116-
section.registerInterceptor(codegenContext.runtimeConfig, this) {
116+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
117117
rustTemplate(
118118
"#{Interceptor}::<#{Input}>::new()",
119119
"Interceptor" to inlineModule.resolve("GlacierAccountIdAutofillInterceptor"),
@@ -122,7 +122,7 @@ private class GlacierOperationInterceptorsCustomization(private val codegenConte
122122
}
123123
}
124124
if (section.operationShape.requiresTreeHashHeader()) {
125-
section.registerInterceptor(codegenContext.runtimeConfig, this) {
125+
section.registerPermanentInterceptor(codegenContext.runtimeConfig, this) {
126126
rustTemplate(
127127
"#{Interceptor}::default()",
128128
"Interceptor" to inlineModule.resolve("GlacierTreeHashHeaderInterceptor"),

0 commit comments

Comments
 (0)