Skip to content

Commit 3a20e42

Browse files
authored
misc: add service-level benchmarks (#1006)
1 parent f8c9121 commit 3a20e42

File tree

21 files changed

+1473
-0
lines changed

21 files changed

+1473
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "2fcce0d9-a174-41ab-bb48-f18bbd5a3c5f",
3+
"type": "misc",
4+
"description": "Add service-level benchmarks",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#968"
7+
]
8+
}

codegen/sdk/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ tasks["jar"].enabled = false
3737
fun getProperty(name: String): String? {
3838
if (project.hasProperty(name)) {
3939
return project.properties[name].toString()
40+
} else if (project.ext.has(name)) {
41+
return project.ext[name].toString()
4042
}
4143

4244
val localProperties = Properties()

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ include(":aws-runtime:aws-config")
3535
include(":aws-runtime:aws-endpoint")
3636
include(":aws-runtime:aws-http")
3737
include(":tests")
38+
include(":tests:benchmarks:service-benchmarks")
3839
include(":tests:codegen:event-stream")
3940
include(":tests:e2e-test-util")
4041

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Service benchmarks
2+
3+
This module is used for benchmarking the performance of generated clients against AWS services. The top 7 services (by
4+
traffic coming from the AWS SDK for Kotlin) are tested and metrics are captured with summaries distilled after the runs
5+
are complete
6+
7+
## Instructions
8+
9+
To run the benchmarks:
10+
* `./gradlew :tests:benchmarks:service-benchmarks:bootstrapAll`
11+
This ensures that all the required service clients are bootstrapped and ready to be built. **You only need to do this
12+
once** in your workspace unless you clean up generated services or make a change to codegen.
13+
* `./gradlew build`
14+
This builds the whole SDK.
15+
* `./gradlew :tests:benchmarks:service-benchmarks:run`
16+
This runs the benchmark suite and prints the results to the console formatted as a Markdown table.
17+
18+
## Baseline as of 8/8/2023
19+
20+
The following benchmark run serves as a baseline for future runs:
21+
22+
### Environment
23+
24+
| Hardware type | Operating system | SDK version |
25+
|----------------|------------------|-----------------|
26+
| EC2 m5.4xlarge | Amazon Linux 2 | 0.30.0-SNAPSHOT |
27+
28+
### Results
29+
30+
| | Overhead (ms) | n | min | avg | med | p90 | p99 | max |
31+
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
32+
| **S3** | | | | | | | | |
33+
| —HeadObject | | 1715 | 0.334 | 0.561 | 0.379 | 0.521 | 3.149 | 20.071 |
34+
| —PutObject | | 739 | 0.306 | 0.492 | 0.337 | 0.389 | 7.958 | 16.556 |
35+
| **SNS** | | | | | | | | |
36+
| —GetTopicAttributes | | 3041 | 0.235 | 0.494 | 0.354 | 0.461 | 2.964 | 17.129 |
37+
| —Publish | | 1001 | 0.199 | 0.394 | 0.224 | 0.420 | 1.262 | 16.160 |
38+
| **STS** | | | | | | | | |
39+
| —AssumeRole | | 1081 | 0.273 | 0.419 | 0.349 | 0.485 | 0.622 | 14.781 |
40+
| —GetCallerIdentity | | 4705 | 0.157 | 0.242 | 0.184 | 0.217 | 0.414 | 13.459 |
41+
| **CloudWatch** | | | | | | | | |
42+
| —GetMetricData | | 1500 | 0.174 | 1.352 | 0.219 | 3.239 | 13.830 | 15.193 |
43+
| —PutMetricData | | 2452 | 0.133 | 1.194 | 0.144 | 1.911 | 13.007 | 14.862 |
44+
| **CloudWatch Events** | | | | | | | | |
45+
| —DescribeEventBus | | 1500 | 0.156 | 0.290 | 0.187 | 0.238 | 0.530 | 18.934 |
46+
| —PutEvents | | 4577 | 0.152 | 0.293 | 0.176 | 0.378 | 3.921 | 10.022 |
47+
| **DynamoDB** | | | | | | | | |
48+
| —GetItem | | 4223 | 0.135 | 0.154 | 0.148 | 0.164 | 0.216 | 2.415 |
49+
| —PutItem | | 3059 | 0.130 | 0.154 | 0.145 | 0.178 | 0.193 | 1.771 |
50+
| **Pinpoint** | | | | | | | | |
51+
| —GetEndpoint | | 555 | 0.220 | 0.401 | 0.406 | 0.452 | 0.506 | 6.606 |
52+
| —PutEvents | | 415 | 0.242 | 0.400 | 0.420 | 0.466 | 0.619 | 2.762 |
53+
54+
## Methodology
55+
56+
This section describes how the benchmarks actually work at a high level:
57+
58+
### Selection criteria
59+
60+
These benchmarks select a handful of services to test against. The selection criterion is the top 7 services by traffic
61+
coming from the AWS SDK for Kotlin (i.e., not from other SDKs, console, etc.). As of 7/28, those top 7 services are S3,
62+
SNS, STS, CloudWatch, CloudWatch Events, DynamoDB, and Pinpoint (in descending order).
63+
64+
For each service, two APIs are selected roughly corresponding to a read and a write operation (e.g., S3::HeadObject is
65+
a read operation and S3::PutObject is a write operation). Efforts are made to ensure that the APIs selected are the top
66+
operations by traffic but alternate APIs may be selected in the case of low throttling limits, high setup complexity,
67+
etc.
68+
69+
### Workflow
70+
71+
Benchmarks are run sequentially in a single thread. This is the high-level workflow for the benchmarks:
72+
73+
* For each benchmark service:
74+
* Instantiate a client with a [special telemetry provider](#telemetry-provider)
75+
* Run any necessary service-specific setup procedures (e.g., create/configure prerequisite resources)
76+
* For each benchmark operation:
77+
* Run any necessary operation-specific setup procedures (e.g., create/configure prerequisite resources)
78+
* Warmup the API call
79+
* Measure the API call
80+
* Aggregate operation metrics
81+
* Run any necessary operation-specific cleanup procedures (e.g., delete resources created in the setup step)
82+
* Run any necessary service-specific cleanup procedures (e.g., delete resources created in the setup step)
83+
* Print overall metrics summary
84+
85+
### Telemetry provider
86+
87+
A custom [benchmark-specific telemetry provider][1] is used to instrument each service client. This provider solely
88+
handles metrics (i.e., no logging, tracing, etc.). It captures specific histogram metrics from an allowlist (currently
89+
only `smithy.client.attempt_overhead_duration`) and aggregates them for the duration of an operation run (not including
90+
the warmup phase). After the run is complete, the metrics are aggregated and various statistics are calculated (e.g.,
91+
minimum, average, median, etc.).
92+
93+
[1]: common/src/aws/sdk/kotlin/benchmarks/service/telemetry/BenchmarkTelemetryProvider.kt
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
buildscript {
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
val atomicFuVersion: String by project
11+
12+
dependencies {
13+
classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicFuVersion")
14+
}
15+
}
16+
17+
plugins {
18+
kotlin("multiplatform")
19+
application
20+
}
21+
22+
application {
23+
mainClass.set("aws.sdk.kotlin.benchmarks.service.BenchmarkHarnessKt")
24+
}
25+
26+
extra.set("skipPublish", true)
27+
28+
val platforms = listOf("common", "jvm")
29+
30+
platforms.forEach { platform ->
31+
apply(from = rootProject.file("gradle/$platform.gradle"))
32+
}
33+
34+
val requiredServices = setOf(
35+
// Top 7 services called by Kotlin SDK customers as of 7/25/2023, in descending order of call volume
36+
"s3",
37+
"sns",
38+
"sts",
39+
"cloudwatch",
40+
"cloudwatchevents",
41+
"dynamodb",
42+
"pinpoint",
43+
44+
// Services required as prerequisites for setup
45+
"iam", // Create roles for STS::AssumeRole
46+
)
47+
48+
val missingServices = requiredServices.filterNot { rootProject.file("services/$it/build.gradle.kts").exists() }
49+
50+
if (missingServices.isEmpty()) {
51+
val optinAnnotations = listOf("kotlin.RequiresOptIn", "aws.smithy.kotlin.runtime.InternalApi")
52+
53+
kotlin {
54+
sourceSets {
55+
all {
56+
val srcDir = if (name.endsWith("Main")) "src" else "test"
57+
val resourcesPrefix = if (name.endsWith("Test")) "test-" else ""
58+
// the name is always the platform followed by a suffix of either "Main" or "Test" (e.g. jvmMain, commonTest, etc)
59+
val platform = name.substring(0, name.length - 4)
60+
kotlin.srcDir("$platform/$srcDir")
61+
resources.srcDir("$platform/${resourcesPrefix}resources")
62+
languageSettings.progressiveMode = true
63+
optinAnnotations.forEach { languageSettings.optIn(it) }
64+
}
65+
66+
val atomicFuVersion: String by project
67+
val coroutinesVersion: String by project
68+
val smithyKotlinVersion: String by project
69+
70+
commonMain {
71+
dependencies {
72+
api("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion")
73+
implementation(project(":aws-runtime:aws-core"))
74+
implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
75+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
76+
77+
requiredServices.forEach { implementation(project(":services:$it")) }
78+
}
79+
}
80+
}
81+
}
82+
} else {
83+
logger.warn(
84+
"Skipping build for {} project, missing the following services: {}. To ensure this project builds, run the " +
85+
"{}:bootstrapAll task.",
86+
project.name,
87+
missingServices.joinToString(", "),
88+
project.path,
89+
)
90+
}
91+
92+
tasks.register("bootstrapAll") {
93+
val bootstrapArg = requiredServices.joinToString(",") { "+$it" }
94+
val bootstrapProj = project(":codegen:sdk")
95+
bootstrapProj.ext.set("aws.services", bootstrapArg)
96+
dependsOn(":codegen:sdk:bootstrap")
97+
}
98+
99+
tasks.named<JavaExec>("run") {
100+
classpath += objects.fileCollection().from(
101+
tasks.named("compileKotlinJvm"),
102+
configurations.named("jvmRuntimeClasspath"),
103+
)
104+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package aws.sdk.kotlin.benchmarks.service
2+
3+
import aws.sdk.kotlin.benchmarks.service.definitions.*
4+
import aws.sdk.kotlin.benchmarks.service.telemetry.MetricSummary
5+
import aws.smithy.kotlin.runtime.client.SdkClient
6+
import aws.smithy.kotlin.runtime.io.use
7+
import kotlin.time.Duration.Companion.seconds
8+
import kotlin.time.ExperimentalTime
9+
import kotlin.time.TimeSource
10+
11+
val DEFAULT_WARMUP_TIME = 5.seconds
12+
val DEFAULT_ITERATION_TIME = 15.seconds
13+
14+
private val benchmarks = setOf(
15+
S3Benchmark(),
16+
SnsBenchmark(),
17+
StsBenchmark(),
18+
CloudwatchBenchmark(),
19+
CloudwatchEventsBenchmark(),
20+
DynamoDbBenchmark(),
21+
PinpointBenchmark(),
22+
).map {
23+
@Suppress("UNCHECKED_CAST")
24+
it as ServiceBenchmark<SdkClient>
25+
}
26+
27+
suspend fun main() {
28+
val harness = BenchmarkHarness()
29+
harness.execute()
30+
}
31+
32+
class BenchmarkHarness {
33+
private val summaries = mutableMapOf<String, MutableMap<String, Map<String, MetricSummary>>>()
34+
35+
suspend fun execute() {
36+
benchmarks.forEach { execute(it) }
37+
println()
38+
printResults()
39+
}
40+
41+
private suspend fun execute(benchmark: ServiceBenchmark<SdkClient>) {
42+
benchmark.client().use { client ->
43+
println("${client.config.clientName}:")
44+
45+
println(" Setting up...")
46+
benchmark.setup(client)
47+
48+
try {
49+
benchmark.operations.forEach { execute(it, client) }
50+
} finally {
51+
benchmark.tearDown(client)
52+
}
53+
}
54+
println()
55+
}
56+
57+
private suspend fun execute(operation: OperationBenchmark<SdkClient>, client: SdkClient) {
58+
println(" ${operation.name}:")
59+
60+
println(" Setting up...")
61+
operation.setup(client)
62+
63+
try {
64+
println(" Warming up for ${operation.warmupMode.explanation}...")
65+
forAtLeast(operation.warmupMode) {
66+
operation.transact(client)
67+
}
68+
69+
Common.metricAggregator.clear()
70+
71+
println(" Measuring for ${operation.iterationMode.explanation}...")
72+
forAtLeast(operation.iterationMode) {
73+
operation.transact(client)
74+
}
75+
76+
val summary = Common.metricAggregator.summarizeAndClear()
77+
summaries.getOrPut(client.config.clientName, ::mutableMapOf)[operation.name] = summary
78+
} finally {
79+
println(" Tearing down...")
80+
operation.tearDown(client)
81+
}
82+
}
83+
84+
private fun printResults() {
85+
val table = ResultsTable.from(summaries)
86+
println(table)
87+
}
88+
}
89+
90+
@OptIn(ExperimentalTime::class)
91+
private inline fun forAtLeast(runMode: RunMode, block: () -> Unit) {
92+
val start = TimeSource.Monotonic.markNow()
93+
94+
when (runMode) {
95+
is RunMode.Time -> {
96+
var cnt = 0
97+
while (start.elapsedNow() < runMode.time) {
98+
block()
99+
cnt++
100+
}
101+
println(" (completed $cnt iterations)")
102+
}
103+
104+
is RunMode.Iterations -> {
105+
repeat(runMode.iterations) {
106+
block()
107+
}
108+
println(" (took ${start.elapsedNow()})")
109+
}
110+
}
111+
}
112+
113+
private val RunMode.explanation get() = when (this) {
114+
is RunMode.Iterations -> "$iterations iterations"
115+
is RunMode.Time -> time.toString()
116+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.benchmarks.service
6+
7+
import aws.sdk.kotlin.benchmarks.service.telemetry.BenchmarkTelemetryProvider
8+
import aws.sdk.kotlin.benchmarks.service.telemetry.MetricAggregator
9+
import aws.smithy.kotlin.runtime.ExperimentalApi
10+
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
11+
import aws.smithy.kotlin.runtime.util.Uuid
12+
13+
object Common {
14+
val metricAggregator = MetricAggregator()
15+
16+
val noRetries = StandardRetryStrategy {
17+
maxAttempts = 1
18+
}
19+
20+
@OptIn(ExperimentalApi::class)
21+
val telemetryProvider = BenchmarkTelemetryProvider(metricAggregator)
22+
23+
fun random(prefix: String = "") = "$prefix${Uuid.random()}"
24+
}

0 commit comments

Comments
 (0)