Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
7470482
Add e2e shared SDK benchmarks
mtdowling May 27, 2026
40f0253
Reduce harness allocations
mtdowling May 28, 2026
6ffe5e8
Ignore profiles
mtdowling May 28, 2026
597e24b
Add virtual-thread-based HTTP client
mtdowling Apr 14, 2026
160e611
Implement CR feedback
mtdowling Apr 20, 2026
1ba92c2
Improve benchmark names and minor fixes
mtdowling Apr 21, 2026
0f44ce0
Optimize client and improve benchmarks
mtdowling Apr 21, 2026
b456814
Use channels and SSLEngine
mtdowling Apr 22, 2026
ea0c1bd
Simplify HTTP client
mtdowling Apr 23, 2026
22d6c63
Continue VT client work
mtdowling Apr 23, 2026
ee72361
Fix apache client buffering
mtdowling May 29, 2026
88f0b7a
Improve channel datastreams
mtdowling May 29, 2026
51aac3f
Used improved channel data streams
mtdowling May 29, 2026
46b3263
Optimize pool and header parsing
mtdowling May 29, 2026
f8403b9
Add more h1 optimizations
mtdowling May 30, 2026
5f87402
Remove some stream wrappers
mtdowling May 30, 2026
c45e546
Add adaptive max conns
mtdowling May 30, 2026
5a61c57
Add WIP DNS round robin
mtdowling May 30, 2026
2da3df7
Remove adaptive max conns
mtdowling May 30, 2026
a6b40eb
Simplify h1 pooling
mtdowling May 30, 2026
44c623a
Fix bench to show ops/s
mtdowling May 30, 2026
908b3d0
Make minor tweaks and style changes
mtdowling May 30, 2026
3cd24a5
fix spotless and compilation
adwsingh May 30, 2026
b2896ae
Fix netty bugs
adwsingh May 31, 2026
eec37ea
Remove dead experiment
mtdowling May 31, 2026
c71d876
Organize better, fix OOM
mtdowling May 31, 2026
ebb846c
Remove unused import
mtdowling May 31, 2026
3a04cd5
Fix some h1 issues and add tests
mtdowling May 31, 2026
710fcb1
Remove some h1 allocations
mtdowling May 31, 2026
b4822a1
Reduce PUT allocations
mtdowling May 31, 2026
fc10de4
Avoid rotation allocation
mtdowling May 31, 2026
4244965
Override jdk defaults
mtdowling May 31, 2026
4ff0496
Remove a few more allocations
mtdowling May 31, 2026
d23586c
Pass line buffer to chunked input stream
mtdowling May 31, 2026
458ab42
Make h1exchange reusable
mtdowling May 31, 2026
35d84c3
Pass chunk buffer in
mtdowling May 31, 2026
077398c
Improve connection validation and selection
mtdowling Jun 1, 2026
5fe437a
Grow HostPool as needed, cleanup Route
mtdowling Jun 1, 2026
601f66a
Share auth across requests
mtdowling Jun 1, 2026
385f1ef
Do not expose h2 load balancer
mtdowling Jun 1, 2026
3775ddf
Remove dead code, move proxy
mtdowling Jun 1, 2026
6c20a10
refactor netty
adwsingh May 31, 2026
101a784
more netty fixes
adwsingh May 31, 2026
9052816
Avoid copies and use hased wheel timer for timeout
adwsingh May 31, 2026
15680df
Add boring ssl to the client
adwsingh May 31, 2026
91ba9e0
Replace SSLEngineTransport per-read Selector with shared HashedWheelT…
adwsingh May 31, 2026
d02ed81
more perf improvements
adwsingh Jun 1, 2026
954f762
Make minor http client cleanup
mtdowling Jun 1, 2026
4dcd8be
Cleanup docs
mtdowling Jun 1, 2026
e629509
apply spotless
adwsingh Jun 1, 2026
1d9e618
reduce copies
adwsingh Jun 1, 2026
a17b87c
Add more sigv4/s3e cleanup
mtdowling Jun 1, 2026
4137287
Optimize workload runner
mtdowling Jun 1, 2026
9ec6c0b
Improve benchmark
mtdowling Jun 2, 2026
f12e9fe
Organize benchmarks
mtdowling Jun 2, 2026
a41455a
Improve benchmarks
mtdowling Jun 2, 2026
8720991
Change signalAll to signal
mtdowling Jun 2, 2026
de13aaa
Add a new epoll channel
adwsingh Jun 2, 2026
fbdfbfb
Fix http benchmarks
mtdowling Jun 2, 2026
c1fe242
Support epoll with h2c and as ConnectTransport; fix benchmarks
mtdowling Jun 2, 2026
5ce933d
Fix benchmark host
mtdowling Jun 3, 2026
216ff5a
Fix epoll buffer position handling
mtdowling Jun 3, 2026
6f7c013
Optimize h2 and fix discard
mtdowling Jun 3, 2026
66baca6
Improve batching and fix up comments
mtdowling Jun 3, 2026
45e6612
Do not over-expose control flow in HttpExchange
mtdowling Jun 3, 2026
4c55c04
Fix a few h2 issues and add regression tests
mtdowling Jun 4, 2026
d100e90
Remove unused context from HTTP client
mtdowling Jun 5, 2026
148a9c8
Remove dead code
mtdowling Jun 5, 2026
1ee011e
Add HTTP listeners for telemetry
mtdowling Jun 5, 2026
043238f
Simplify HTTP client config, fix boringSSL alpn
mtdowling Jun 7, 2026
d96e1f0
Drop carrier-pinning synchronized from H2Exchange body
mtdowling Jun 7, 2026
cedb414
Throw InterruptedIOException from H2StreamBody on interrupt
mtdowling Jun 7, 2026
6f1e2d8
Fix close error terminal; cover remaining read failure paths
mtdowling Jun 8, 2026
d4470bf
Fix gradle conventions
mtdowling Jun 11, 2026
cc7b919
Introduce TlsProvider SPI for the HTTP client
mtdowling Jun 19, 2026
3b8f2b7
Move SSLSocket fast path into JdkTlsProvider
mtdowling Jun 19, 2026
e2c60d1
Add per-request overrides to RequestOptions
mtdowling Jun 19, 2026
ac05d14
Remove WIP clients
mtdowling Jun 22, 2026
bcfcaf2
Fix HTTP/2 end-of-body on HEADERS frames (trailers + empty-body race)
mtdowling Jun 25, 2026
da1af60
Suppress false-positive NN_NAKED_NOTIFY on H2StreamBody.signal()
mtdowling Jun 25, 2026
a079570
Fix remaining SpotBugs findings in http-client
mtdowling Jun 25, 2026
7a16960
Add integ test proving H2 bidirectional (full-duplex) streaming
mtdowling Jun 25, 2026
3e9c80e
Flush HTTP/2 request body per event-stream message
mtdowling Jun 25, 2026
cbaaf25
Trim down interfaces and cleanup docs
mtdowling Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 11 additions & 0 deletions .github/workflows/fuzz-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ jobs:
run: |
./gradlew :rulesengine:test --tests "*FuzzTest*" -Djazzer.max_duration=1h --stacktrace

- name: Run HPACK fuzz tests
env:
JAZZER_FUZZ: "1"
run: |
./gradlew :http:http-hpack:test --tests "*FuzzTest*" -Djazzer.max_duration=1h --stacktrace

- name: Run HTTP client fuzz tests
env:
JAZZER_FUZZ: "1"
run: |
./gradlew :http:http-client:test --tests "*FuzzTest*" -Djazzer.max_duration=1h --stacktrace
- name: Save fuzz corpus cache
uses: actions/cache/save@v6
if: always()
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Ignore Gradle project-specific cache directory
.gradle

.tool-versions

# Ignore kotlin cache dir
.kotlin

Expand Down Expand Up @@ -28,3 +30,5 @@ mise.toml
.claude/settings.local.json

**/bin

benchmarks/e2e-benchmarks/profiles
113 changes: 95 additions & 18 deletions benchmarks/e2e-benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# smithy-java end-to-end benchmark runner

A workload-driven runner that exercises the smithy-java SDK against live AWS
services. It implements the same workload spec as the Java SDK v2 reference
runner so results are directly comparable across SDKs.
A small fixed-workload runner that exercises the smithy-java SDK against live AWS services.

## Scope

Expand All @@ -13,20 +11,13 @@ runner so results are directly comparable across SDKs.
| S3 | PutObject | Throughput|
| S3 | GetObject | Throughput|

Only the **synchronous** client mode is supported. smithy-java does not
generate async clients today, so a `--client async` argument is accepted but
prints a warning and proceeds with the sync client. Throughput tests still
push the SDK through a thread pool, which is the SDK's idiomatic concurrency
mechanism.

## Build

```bash
./gradlew :benchmarks:e2e-benchmarks:shadowJar
```

The jar lands at
`benchmarks/e2e-benchmarks/build/libs/smithy-java-e2e-benchmark-runner.jar`.
The jar lands at `benchmarks/e2e-benchmarks/build/libs/smithy-java-e2e-benchmark-runner.jar`.

The build pulls AWS service models from Maven (`software.amazon.api.models:dynamodb`
and `software.amazon.api.models:s3`) and codegens the typed DynamoDB and S3
Expand All @@ -37,16 +28,102 @@ don't collide.

```bash
java -jar build/libs/smithy-java-e2e-benchmark-runner.jar \
workloads/ddb-getitem-1KiB-latency-benchmark.json us-east-1
--operation s3-put \
--bucket my-bench--use1-az4--x-s3 \
--region us-east-1
```

Operations:

| Operation | Workload |
|-----------|----------|
| `s3-put` | S3 PutObject, 256 KiB body, concurrent |
| `s3-get` | S3 GetObject, 256 KiB body, concurrent |
| `ddb-put` | DynamoDB PutItem, 1 KiB item, sequential |
| `ddb-get` | DynamoDB GetItem, 1 KiB item, sequential |

Flags:

| Flag | Notes |
|------|-------|
| `--operation <name>` | One of `s3-put`, `s3-get`, `ddb-put`, `ddb-get` |
| `--bucket <name>` | S3 Express directory bucket: `<base>--<az>--x-s3` |
| `--table <name>` | Must have a `String` partition key named `pk` |
| `--region <region>` | Use the region the bucket / table lives in |
| `--client sync\|async` | Accepted for compatibility; smithy-java only supports sync |
| `--key-prefix <prefix>` | DynamoDB key prefix, default `item-` |
| `--s3-key-prefix <prefix>` | S3 key prefix, default `objects/256KiB/` |

Common system properties:

| Property | Default |
|----------|---------|
| `e2e.batch.actions` | `10000` for S3, `1000` for DynamoDB |
| `e2e.warmup.batches` | `2` |
| `e2e.measurement.batches` | `3` |
| `e2e.collectMetrics` | `true` |
| `e2e.object.size` | `262144` |
| `e2e.data.length` | `1024` |
| `e2e.ddb.createTable` | `true` |
| `e2e.ddb.deleteTable` | `true` |
| `e2e.ddb.readCapacityUnits` | `5000` |
| `e2e.ddb.writeCapacityUnits` | `5000` |

## Provision the resources

Run from an EC2 instance in the same AZ as the bucket. The instance role
needs `s3:CreateSession`, `s3express:*`, and `dynamodb:GetItem`/`PutItem`
on the resources below.

S3 Express directory bucket (replace `my-bench` and `use1-az4` with your
own base name and availability zone):

```bash
aws s3api create-bucket \
--bucket my-bench--use1-az4--x-s3 \
--region us-east-1 \
--create-bucket-configuration '{
"Location": {"Type": "AvailabilityZone", "Name": "use1-az4"},
"Bucket": {"Type": "Directory", "DataRedundancy": "SingleAvailabilityZone"}
}'
```

Region passed on the command line is ignored — the workload JSON is the
source of truth.
The S3 download workload reads keys named `objects/256KiB/<index>`. Pre-seed
them by running the upload workload once first.

You can point the runner at any v1 workload JSON.
The DynamoDB workloads create a fresh provisioned table by default using
`--table` as the base name. The actual table name gets a run-specific suffix,
uses a `String` partition key named `pk`, defaults to `5000` RCU / `5000` WCU,
and is deleted after the run. The GetItem workload seeds its keys before warmup.

To reuse an existing table instead:

```bash
java -De2e.ddb.createTable=false -jar build/libs/smithy-java-e2e-benchmark-runner.jar \
--operation ddb-get --table my-bench-table --region us-east-1
```

## Cleanup

```bash
aws s3 rm s3://my-bench--use1-az4--x-s3 --recursive --region us-east-1
aws s3api delete-bucket --bucket my-bench--use1-az4--x-s3 --region us-east-1
aws dynamodb delete-table --table-name my-bench-table --region us-east-1
```

## Concurrency

Throughput benchmarks dispatch each batch action onto a virtual thread,
gated by a semaphore. The cap is `cores × multiplier`, where `multiplier`
defaults to `4` and can be overridden:

```bash
java -De2e.concurrency.multiplier=16 -jar build/libs/smithy-java-e2e-benchmark-runner.jar \
--operation s3-put --bucket my-bench--use1-az4--x-s3 --region us-east-1
```

## Credentials

Credentials come from the AWS SDK v2 default credential provider chain
(env vars, profile file, container roles, IMDS). Override via the standard
SDK v2 environment variables.
Credentials are resolved from EC2 IMDSv2 directly. The benchmark is meant
to run on EC2 against directory buckets in the same AZ. For local testing
or non-EC2 hosts, supply credentials by extending the `Clients` factory.
29 changes: 17 additions & 12 deletions benchmarks/e2e-benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Note: Not published
plugins {
id("smithy-java.java-conventions")
id("com.gradleup.shadow")
Expand All @@ -7,9 +8,6 @@ plugins {

description = "End-to-end SDK benchmarks against live AWS services (DynamoDB GetItem/PutItem latency, S3 GetObject/PutObject throughput)."

// Not published. Mirrors the Java SDK v2 reference runner and reads the same
// workload JSON files so results are directly comparable.

application {
mainClass.set("software.amazon.smithy.java.benchmarks.e2e.WorkloadRunner")
}
Expand All @@ -19,6 +17,11 @@ dependencies {
smithyBuild(project(":codegen:codegen-plugin"))
smithyBuild(project(":client:client-core"))
smithyBuild(project(":client:client-waiters"))
// Needed at codegen time so RulesEngineBuilder loads AwsRulesExtension via ServiceLoader.
smithyBuild(project(":aws:client:aws-client-rulesengine"))
// Codegen needs the plugin class on its classpath so `Class.forName(...)` resolves it
// when wiring it into the generated client.
smithyBuild(project(":aws:aws-sigv4-s3express"))

// AWS service models pulled from Maven (https://github.com/aws/api-models-aws).
// The smithy-base plugin only loads models from sources + runtimeClasspath
Expand Down Expand Up @@ -48,6 +51,7 @@ dependencies {

// AWS-specific runtime: SigV4, AWS protocols, AWS endpoints.
implementation(project(":aws:aws-sigv4"))
implementation(project(":aws:aws-sigv4-s3express"))
implementation(project(":aws:aws-auth-api"))
implementation(project(":aws:client:aws-client-core"))
implementation(project(":aws:client:aws-client-http"))
Expand All @@ -65,18 +69,18 @@ dependencies {
implementation(libs.smithy.aws.traits)
implementation(libs.smithy.model)

// smithy-java native credential chain — covers env vars, system props,
// shared config, web identity token, and ECS container slots out of the
// box; pulling in aws-credentials-imds adds the EC2 instance-metadata
// provider on top of it. Both modules register their providers via
// ServiceLoader, so just having them on the classpath is enough.
// smithy-java credential chain
implementation(project(":aws:aws-credential-chain"))
implementation(project(":aws:aws-credentials-imds"))

// Alternate transports — selected at runtime via -De2e.transport=smithy|smithy-boringssl
implementation(project(":client:client-http-smithy"))
implementation(project(":client:client-http-boringssl"))
}

// Two projections so that DynamoDB and S3 generate into different namespaces
// and don't collide. Each projection filters down to just the ONE service
// it wants the model JAR for s3 only has the s3 model, but the projection
// and don't collide. Each projection filters down to just the one service
// it wants; the model JAR for s3 only has the s3 model, but the projection
// makes the intent explicit and gives us a stable name.
val codegenProjections = listOf("dynamodb-client", "s3-client")

Expand Down Expand Up @@ -104,7 +108,8 @@ tasks.named<Copy>("processResources") {
dependsOn("smithyBuild")
}

// The shaded jar is the self-contained artifact users invoke to run a workload.
// The shaded jar is what users invoke from run-benchmark.py, mirroring
// `java -jar runners/java-workload-runner/target/workload-runner-1.0.0.jar`.
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveBaseName.set("smithy-java-e2e-benchmark-runner")
archiveClassifier.set("")
Expand All @@ -123,7 +128,7 @@ tasks.named("assemble") {
dependsOn("shadowJar")
}

// Don't run benchmarks under `./gradlew check` they hit live AWS.
// Don't run benchmarks under `./gradlew check`, they hit live AWS.
tasks.named("check") {
enabled = true
}
14 changes: 12 additions & 2 deletions benchmarks/e2e-benchmarks/smithy-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
"transport": {
"http-java": {}
},
"modes": ["client"]
"modes": ["client"],
"runtimeTraits": [
"smithy.rules#contextParam",
"smithy.rules#staticContextParams",
"smithy.rules#operationContextParams"
]
}
}
},
Expand All @@ -47,7 +52,12 @@
"transport": {
"http-java": {}
},
"modes": ["client"]
"modes": ["client"],
"runtimeTraits": [
"smithy.rules#contextParam",
"smithy.rules#staticContextParams",
"smithy.rules#operationContextParams"
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package software.amazon.smithy.java.benchmarks.e2e;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Map;
import software.amazon.smithy.java.benchmarks.e2e.dynamodb.client.DynamoDBClient;
Expand All @@ -19,10 +18,7 @@
import software.amazon.smithy.java.io.datastream.DataStream;

/**
* Wraps the smithy-java DynamoDB and S3 clients with the four operations the
* benchmark exercises. Smithy-java currently only generates synchronous
* clients, so the throughput tests achieve concurrency by submitting work to
* a thread pool from {@link WorkloadRunner}.
* Wraps the smithy-java DynamoDB and S3 clients with the four operations the benchmark exercises.
*/
final class ActionExecutor {

Expand Down Expand Up @@ -51,9 +47,8 @@ void getItem(String tableName, Map<String, AttributeValue> key) {
}

void putObject(String bucket, String key, int objectSize) {
// The reference runner reuses a single in-memory body across all
// uploads. DataStream.ofBytes wraps the array without copying, so
// each call is a thin handle over the same buffer.
// The reference runner reuses a single in-memory body across all uploads.
// DataStream.ofBytes wraps the array without copying, so each call is a thin handle over the same buffer.
var body = DataStream.ofBytes(payload, 0, objectSize, "application/octet-stream");
s3.putObject(PutObjectInput.builder()
.bucket(bucket)
Expand All @@ -68,19 +63,14 @@ void getObject(String bucket, String key) {
.bucket(bucket)
.key(key)
.build());
// Drain the body so the download time is what we measure, not just
// the response headers. writeTo(nullOutputStream) walks the stream
// without forcing a single-contiguous-ByteBuffer materialization, so
// we exercise the SDK's most efficient consume path (multi-chunk
// delivery from the wire is fed straight through with no stitch).
// discard() drains and releases the underlying source.
var body = output.getBody();
if (body != null) {
try {
body.writeTo(OutputStream.nullOutputStream());
body.discard();
} catch (IOException e) {
throw new UncheckedIOException("Failed to drain S3 GetObject body", e);
throw new UncheckedIOException("Failed to discard S3 GetObject body", e);
}
}
}

}
Loading
Loading