diff --git a/.github/workflows/performance-benchmarks-java.yml b/.github/workflows/performance-benchmarks-java.yml new file mode 100644 index 000000000..908472bec --- /dev/null +++ b/.github/workflows/performance-benchmarks-java.yml @@ -0,0 +1,142 @@ +# This workflow runs every day 09:00 UTC (1AM PST) +name: Performance Benchmarks + +on: + workflow_call: + inputs: + dafny: + description: "The Dafny version to run" + required: false + default: "4.9.0" + type: string + regenerate-code: + description: "Regenerate code using smithy-dafny" + required: false + default: false + type: boolean + mpl-version: + description: "MPL version to use" + required: false + type: string + mpl-head: + description: "Running on MPL HEAD" + required: false + default: false + type: boolean +jobs: + testJava: + strategy: + fail-fast: false + matrix: + library: [DynamoDbEncryption] + benchmark-dir: [db-esdk-performance-testing] + java-version: [8] + os: [macos-14] + runs-on: ${{ matrix.os }} + permissions: + id-token: write + contents: read + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v5 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::370957321024:role/GitHub-CI-DDBEC-Dafny-Role-us-west-2 + role-session-name: DDBEC-Performance-Benchmarks-Java + + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Setup Dafny + uses: ./submodules/MaterialProviders/.github/actions/setup_dafny/ + with: + dafny-version: ${{ inputs.dafny }} + + - name: Update MPL submodule if using MPL HEAD + if: ${{ inputs.mpl-head == true }} + working-directory: submodules/MaterialProviders + run: | + git checkout main + git pull + git submodule update --init --recursive + git rev-parse HEAD + + - name: Update project.properties if using MPL HEAD + if: ${{ inputs.mpl-head == true }} + run: | + sed "s/mplDependencyJavaVersion=.*/mplDependencyJavaVersion=${{inputs.mpl-version}}/g" project.properties > project.properties2; mv project.properties2 project.properties + + - name: Install Smithy-Dafny codegen dependencies + uses: ./.github/actions/install_smithy_dafny_codegen_dependencies + + - name: Regenerate code using smithy-dafny if necessary + if: ${{ inputs.regenerate-code }} + uses: ./.github/actions/polymorph_codegen + with: + dafny: ${{ env.DAFNY_VERSION }} + library: ${{ matrix.library }} + diff-generated-code: false + update-and-regenerate-mpl: true + + - name: Setup Java 8 + uses: actions/setup-java@v5 + with: + distribution: "corretto" + java-version: 8 + + - name: Setup Java ${{ matrix.java-version }} + uses: actions/setup-java@v5 + with: + distribution: "corretto" + java-version: ${{ matrix.java-version }} + + - name: Build ${{ matrix.library }} implementation + shell: bash + working-directory: ./${{ matrix.library }} + run: | + # This works because `node` is installed by default on GHA runners + CORES=$(node -e 'console.log(os.cpus().length)') + make build_java CORES=$CORES + make mvn_local_deploy + + - name: Run Performance Benchmarks - Quick Mode + shell: bash + working-directory: ./${{matrix.benchmark-dir}}/benchmarks/java + run: | + ./gradlew run --args="--config ../config/test-scenarios.yaml --quick" + ./gradlew run --args="--config ../config/test-scenarios.yaml --quick --legacy-override" + + - name: Parse and Format Logs + working-directory: ./${{matrix.benchmark-dir}}/benchmarks/results/raw-data/ + run: | + LOG_FILE="java_results.json" + DDBEC_LOG_FILE="java_ddbec_results.json" + UPLOAD_FILE="cloudwatch_logs.json" + DDBEC_UPLOAD_FILE="cloudwatch_ddbec_log.json" + TIMESTAMP=$(date +%s%3N) + jq -c --arg ts "$(date +%s)000" '[.results[] as $result | .metadata as $meta | {timestamp: ($ts | tonumber), message: ({metadata: $meta, result: $result} | tostring)}]' $LOG_FILE > $UPLOAD_FILE + jq -c --arg ts "$(date +%s)000" '[.results[] as $result | .metadata as $meta | {timestamp: ($ts | tonumber), message: ({metadata: $meta, result: $result} | tostring)}]' $DDBEC_LOG_FILE > $DDBEC_UPLOAD_FILE + + - name: Upload logs to CloudWatch + working-directory: ./${{matrix.benchmark-dir}}/benchmarks/results/raw-data/ + run: | + LOG_FILE="cloudwatch_logs.json" + DDBEC_LOG_FILE="cloudwatch_ddbec_log.json" + LOG_GROUP="aws-dbesdk-performance-benchmarks" + LOG_STREAM="java/${{matrix.java-version}}/quick_benchmarks/${{ github.workflow }}" + + # Create log stream (ignore if exists) + aws logs create-log-stream \ + --log-group-name "$LOG_GROUP" \ + --log-stream-name "$LOG_STREAM" 2>/dev/null || true + + aws logs put-log-events \ + --log-group-name "$LOG_GROUP" \ + --log-stream-name "$LOG_STREAM" \ + --log-events file://$LOG_FILE + + aws logs put-log-events \ + --log-group-name "$LOG_GROUP" \ + --log-stream-name "$LOG_STREAM" \ + --log-events file://$DDBEC_LOG_FILE diff --git a/.github/workflows/performance-benchmarks.yml b/.github/workflows/performance-benchmarks.yml index eb82758e5..c1da01287 100644 --- a/.github/workflows/performance-benchmarks.yml +++ b/.github/workflows/performance-benchmarks.yml @@ -21,13 +21,18 @@ jobs: uses: ./.github/workflows/performance-benchmarks-go.yml with: dafny: ${{needs.getVersion.outputs.version}} + performance-benchmarks-java: + needs: getVersion + uses: ./.github/workflows/performance-benchmarks-java.yml + with: + dafny: ${{needs.getVersion.outputs.version}} performance-benchmarks-rust: needs: getVersion uses: ./.github/workflows/performance-benchmarks-rust.yml with: dafny: ${{needs.getVersion.outputs.version}} notify: - needs: [getVersion, performance-benchmarks-go, performance-benchmarks-rust] + needs: [getVersion, performance-benchmarks-go, performance-benchmarks-rust, performance-benchmarks-java] if: ${{ failure() }} uses: aws/aws-cryptographic-material-providers-library/.github/workflows/slack-notification.yml@main with: diff --git a/cfn/CW-Filters.yml b/cfn/CW-Filters.yml index 6539d237a..cc3fd2936 100644 --- a/cfn/CW-Filters.yml +++ b/cfn/CW-Filters.yml @@ -603,6 +603,125 @@ Resources: - Key: Language Value: "$.result.language" + # Filter 1: Memory Efficiency Ratio - JavaDDBEC + JavaDDBECMemoryEfficiencyRatioFilter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.result.language = "java-ddbec-native" && $.result.test_name = "memory" }' + MetricTransformations: + - MetricName: MemoryEfficiencyRatio + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.memory_efficiency_ratio" + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 2: Memory Usage 50MB - JavaDDBEC + JavaDDBECMemoryUsage50MBFilter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.result.language = "java-ddbec-native" && $.result.test_name = "memory" }' + MetricTransformations: + - MetricName: PeakMemoryMB + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.peak_memory_mb" + Unit: Megabytes + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 3: DataSize 52MB P50 Latency - JavaDDBEC + JavaDDBECDataSize52MBP50LatencyFilter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.metadata.language = "java-ddbec-native" && $.result.test_name = "throughput" }' + MetricTransformations: + - MetricName: P50Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p50_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 4: DataSize 52MB P95 Latency - JavaDDBEC + JavaDDBECDataSize52MBP95LatencyFilter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.metadata.language = "java-ddbec-native" && $.result.test_name = "throughput" }' + MetricTransformations: + - MetricName: P95Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p95_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 5: DataSize 52MB P99 Latency - JavaDDBEC + JavaDDBECDataSize52MBP99LatencyFilter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.metadata.language = "java-ddbec-native" && $.result.test_name = "throughput" }' + MetricTransformations: + - MetricName: P99Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p99_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 6: Concurrency P50 - JavaDDBEC + JavaDDBECConcurrencyP50Filter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.result.language = "java-ddbec-native" && $.result.test_name = "concurrent" }' + MetricTransformations: + - MetricName: ConcurrencyP50Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p50_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 7: Concurrency P95 - JavaDDBEC + JavaDDBECConcurrencyP95Filter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.result.language = "java-ddbec-native" && $.result.test_name = "concurrent" }' + MetricTransformations: + - MetricName: ConcurrencyP95Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p95_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + + # Filter 8: Concurrency P99 - JavaDDBEC + JavaDDBECConcurrencyP99Filter: + Type: AWS::Logs::MetricFilter + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: '{ $.result.language = "java-ddbec-native" && $.result.test_name = "concurrent" }' + MetricTransformations: + - MetricName: ConcurrencyP99Latency + MetricNamespace: JavaDDBECPerformanceBenchmarks + MetricValue: "$.result.p99_latency" + Unit: Milliseconds + Dimensions: + - Key: Language + Value: "$.result.language" + Outputs: LogGroupName: Description: "CloudWatch Log Group Name" @@ -612,8 +731,8 @@ Outputs: TotalFiltersCreated: Description: "Total number of metric filters created" - Value: "40" + Value: "48" MetricNamespaces: Description: "CloudWatch Metrics Namespaces by Language" - Value: "JavaDBESDKPerformanceBenchmarks, NetDBESDKPerformanceBenchmarks, PythonDBESDKPerformanceBenchmarks, GoDBESDKPerformanceBenchmarks, RustDBESDKPerformanceBenchmarks" + Value: "JavaDBESDKPerformanceBenchmarks, NetDBESDKPerformanceBenchmarks, PythonDBESDKPerformanceBenchmarks, GoDBESDKPerformanceBenchmarks, RustDBESDKPerformanceBenchmarks, JavaDDBECPerformanceBenchmarks" diff --git a/db-esdk-performance-testing/benchmarks/java/.gitignore b/db-esdk-performance-testing/benchmarks/java/.gitignore new file mode 100644 index 000000000..fc0da9928 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/.gitignore @@ -0,0 +1,15 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore bin +bin + +# JetBrains +.idea/* +*.iml + +# Mac OS X +.DS_Store diff --git a/db-esdk-performance-testing/benchmarks/java/README.md b/db-esdk-performance-testing/benchmarks/java/README.md new file mode 100644 index 000000000..e38c4bcc9 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/README.md @@ -0,0 +1,219 @@ +# DB-ESDK Java Benchmark + +Performance benchmark suite for the AWS Database Encryption SDK (DB-ESDK) Java implementation. + +## Quick Start + +```bash +# Run quick benchmark +./gradlew run --args="--config ../config/test-scenarios.yaml --quick" + +# Run full benchmark +./gradlew run --args="--config ../config/test-scenarios.yaml" + +# Run with custom output path +./gradlew run --args="--config ../config/test-scenarios.yaml --output ../results/raw-data/my-java-results.json" +``` + +## Build + +```bash +# Build the project +./gradlew build + +# Build fat JAR for standalone execution +./gradlew fatJar + +# Run the fat JAR +java -jar build/libs/db-esdk-benchmark-java-1.0.0-all.jar --quick +``` + +## Configuration + +The benchmark uses YAML configuration files. See `../config/test-scenarios.yaml` for the full configuration format. + +### Quick Mode + +Quick mode runs a subset of tests with reduced iterations: + +- Only runs test types specified in `quick_config.test_types` +- Uses smaller data sizes from `quick_config.data_sizes.small` +- Fewer iterations: `quick_config.iterations.measurement` + +### Configuration Structure + +```yaml +# Data sizes to test (in bytes) +data_sizes: + small: [1024, 5120, 10240] # 1KB, 5KB, 10KB + medium: [102400, 512000, 1048576] # 100KB, 500KB, 1MB + large: [10485760, 52428800, 104857600] # 10MB, 50MB, 100MB + +# Test iterations +iterations: + warmup: 5 # Warmup iterations (not counted) + measurement: 10 # Measurement iterations + +# Concurrency levels to test +concurrency_levels: [1, 2, 4, 8, 16] + +# DynamoDB table name +table_name: "dbesdk-performance-testing" + +# Keyring type +keyring: "raw-aes" + +# Quick test configuration +quick_config: + data_sizes: + small: [102400] # 100KB only for quick mode + iterations: + warmup: 3 + measurement: 3 + concurrency_levels: [1, 2] + test_types: ["throughput", "memory", "concurrency"] +``` + +## Test Types + +- **throughput**: Measures operations per second, latency percentiles (P50/P95/P99), and separate put/get latencies +- **memory**: Measures peak memory usage during operations with continuous sampling +- **concurrency**: Tests performance under concurrent load with multiple threads + +## Command Line Options + +``` +Usage: java -jar db-esdk-benchmark.jar [options] + +Options: + -c,--config Path to test configuration file (default: ../config/test-scenarios.yaml) + -h,--help Show this help message + -o,--output Path to output results file (default: ../results/raw-data/java_results.json) + -q,--quick Run quick test with reduced iterations +``` + +## Output Format + +Results are saved to JSON format matching the Go implementation: + +```json +{ + "metadata": { + "language": "java", + "timestamp": "2025-01-21 10:50:00", + "java_version": "11.0.16", + "cpu_count": 8, + "total_memory_gb": 16.0, + "total_tests": 22 + }, + "results": [ + { + "test_name": "throughput", + "language": "java", + "data_size": 1024, + "concurrency": 1, + "put_latency_ms": 1.2, + "get_latency_ms": 1.1, + "end_to_end_latency_ms": 2.3, + "ops_per_second": 426.4, + "bytes_per_second": 436641.2, + "peak_memory_mb": 0, + "memory_efficiency_ratio": 0, + "p50_latency": 1.8, + "p95_latency": 1.81, + "p99_latency": 1.82, + "timestamp": "2025-01-21 10:45:30", + "java_version": "11.0.16", + "cpu_count": 8, + "total_memory_gb": 16.0 + } + ] +} +``` + +## Architecture + +The Java implementation mirrors the Go benchmark architecture: + +### Core Components + +- **`Program.java`**: Main entry point with CLI argument parsing +- **`DBESDKBenchmark.java`**: Main benchmark orchestration and execution +- **`TestConfig.java`**: YAML configuration parsing and management +- **`BenchmarkResult.java`**: Results data structure matching Go output format +- **`KeyringSetup.java`**: Keyring initialization (Raw AES keyring) +- **`Utils.java`**: Utility functions for statistics, memory monitoring, and formatting + +### Test Implementation + +- **Throughput Tests**: Measure `EncryptItem` and `DecryptItem` operations separately +- **Memory Tests**: Continuous heap monitoring during operations using JVM MemoryMXBean +- **Concurrency Tests**: ExecutorService-based multi-threaded testing + +### DynamoDB Item Structure + +The benchmark tests encryption of DynamoDB items with this structure: + +```java +{ + "partition_key": "benchmark-test", // SIGN_ONLY + "sort_key": "0", // SIGN_ONLY + "attribute1": { // ENCRYPT_AND_SIGN + "data": + }, + "attribute2": "sign me!", // SIGN_ONLY + ":attribute3": "ignore me!" // DO_NOTHING (prefix excludes from encryption) +} +``` + +## Dependencies + +- **AWS Database Encryption SDK for DynamoDB**: Core encryption functionality +- **AWS Cryptographic Material Providers Library**: Keyring management +- **AWS SDK for Java v2**: DynamoDB types and utilities +- **SnakeYAML**: Configuration file parsing +- **Jackson**: JSON output formatting +- **Apache Commons CLI**: Command line argument parsing + +## System Requirements + +- **Java 11+**: Minimum supported version +- **Gradle**: Build system (wrapper included) +- **Memory**: At least 2GB heap recommended for large data size tests +- **CPU**: Multi-core recommended for concurrency tests + +## Comparison with Go Implementation + +The Java implementation provides identical functionality to the Go version: + +- **Same test types**: throughput, memory, concurrency +- **Same data sizes**: 1KB to 100MB range +- **Same configuration format**: YAML with quick mode support +- **Same output format**: JSON with matching field names and structure +- **Same keyring setup**: Raw AES-256 keyring with random key generation +- **Same item structure**: Identical DynamoDB attribute encryption policies + +## Performance Notes + +- **JVM Warmup**: The benchmark includes warmup iterations to account for JIT compilation +- **Memory Measurement**: Uses JVM MemoryMXBean for accurate heap usage tracking +- **Garbage Collection**: Forces GC between memory test iterations for consistent measurements +- **Concurrency**: Uses ExecutorService thread pools for controlled concurrent testing + +## Examples + +```bash +# Quick benchmark with default settings +./gradlew run --args="--quick" + +# Full benchmark with custom configuration +./gradlew run --args="-c /path/to/custom-config.yaml -o /path/to/results.json" + +# Run only specific test types (modify config file) +# Edit config.yaml to set quick_config.test_types: ["throughput"] +./gradlew run --args="--quick" + +# Build standalone JAR and run +./gradlew fatJar +java -Xmx4g -jar build/libs/db-esdk-benchmark-java-1.0.0-all.jar --quick +``` diff --git a/db-esdk-performance-testing/benchmarks/java/build.gradle.kts b/db-esdk-performance-testing/benchmarks/java/build.gradle.kts new file mode 100644 index 000000000..c8c4ca755 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/build.gradle.kts @@ -0,0 +1,134 @@ +import java.io.File +import java.io.FileInputStream +import java.util.Properties +import java.net.URI +import javax.annotation.Nullable +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +tasks.wrapper { + gradleVersion = "7.6" +} + +plugins { + `java` + `java-library` + `maven-publish` + application +} + +var props = Properties().apply { + load(FileInputStream(File(rootProject.rootDir, "../../../project.properties"))) +} + +var mplVersion = props.getProperty("mplDependencyJavaVersion") +var ddbecVersion = props.getProperty("projectJavaVersion") +var dafnyRuntimeJavaVersion = props.getProperty("dafnyRuntimeJavaVersion") +var smithyDafnyJavaConversionVersion = props.getProperty("smithyDafnyJavaConversionVersion") + + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(8)) +} + +application { + mainClass.set("com.amazon.dbesdk.benchmark.Program") +} + +var caUrl: URI? = null +@Nullable +val caUrlStr: String? = System.getenv("CODEARTIFACT_REPO_URL") +if (!caUrlStr.isNullOrBlank()) { + caUrl = URI.create(caUrlStr) +} + +var caPassword: String? = null +@Nullable +val caPasswordString: String? = System.getenv("CODEARTIFACT_TOKEN") +if (!caPasswordString.isNullOrBlank()) { + caPassword = caPasswordString +} + +repositories { + mavenLocal() + maven { + name = "DynamoDB Local Release Repository - US West (Oregon) Region" + url = URI.create("https://s3-us-west-2.amazonaws.com/dynamodb-local/release") + } + mavenLocal() + mavenCentral() + if (caUrl != null && caPassword != null) { + maven { + name = "CodeArtifact" + url = caUrl!! + credentials { + username = "aws" + password = caPassword!! + } + } + } +} + +// Configuration to hold SQLLite information. +// DynamoDB-Local needs to have access to native sqllite4java. +val dynamodb by configurations.creating + +dependencies { + implementation("org.dafny:DafnyRuntime:${dafnyRuntimeJavaVersion}") + implementation("software.amazon.smithy.dafny:conversion:${smithyDafnyJavaConversionVersion}") + implementation("software.amazon.cryptography:aws-cryptographic-material-providers:${mplVersion}") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:${ddbecVersion}") + + implementation(platform("software.amazon.awssdk:bom:2.30.18")) + implementation("software.amazon.awssdk:dynamodb") + implementation("software.amazon.awssdk:dynamodb-enhanced") + implementation("software.amazon.awssdk:core:2.30.18") + implementation("software.amazon.awssdk:kms") + + // Apache Commons CLI for command line parsing + implementation("commons-cli:commons-cli:1.5.0") + + // SnakeYAML for YAML configuration parsing + implementation("org.yaml:snakeyaml:2.0") + + // Jackson for JSON output + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") + + testImplementation("com.amazonaws:DynamoDBLocal:2.+") + // This is where we gather the SQLLite files to copy over + dynamodb("com.amazonaws:DynamoDBLocal:2.+") + // As of 1.21.0 DynamoDBLocal does not support Apple Silicon + // This checks the dependencies and adds a native library + // to support this architecture. + if (org.apache.tools.ant.taskdefs.condition.Os.isArch("aarch64")) { + testImplementation("io.github.ganadist.sqlite4java:libsqlite4java-osx-aarch64:1.0.392") + dynamodb("io.github.ganadist.sqlite4java:libsqlite4java-osx-aarch64:1.0.392") + } +} + +tasks.test { + useJUnitPlatform() +} + +tasks.withType { + options.encoding = "UTF-8" +} + +// Create a fat JAR for easy execution +tasks.register("fatJar") { + archiveClassifier.set("all") + from(sourceSets.main.get().output) + dependsOn(configurations.runtimeClasspath) + from({ + configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) } + }) { + exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA") + } + manifest { + attributes["Main-Class"] = "com.amazon.dbesdk.benchmark.Program" + } +} + +tasks.named("run") { + standardInput = System.`in` +} diff --git a/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.jar b/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1b33c55ba Binary files /dev/null and b/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.properties b/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ca025c83a --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/db-esdk-performance-testing/benchmarks/java/gradlew b/db-esdk-performance-testing/benchmarks/java/gradlew new file mode 100755 index 000000000..23d15a936 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/db-esdk-performance-testing/benchmarks/java/gradlew.bat b/db-esdk-performance-testing/benchmarks/java/gradlew.bat new file mode 100644 index 000000000..5eed7ee84 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/db-esdk-performance-testing/benchmarks/java/settings.gradle.kts b/db-esdk-performance-testing/benchmarks/java/settings.gradle.kts new file mode 100644 index 000000000..d1a150c7d --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "db-esdk-benchmark-java" diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/BenchmarkResult.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/BenchmarkResult.java new file mode 100644 index 000000000..f5f69f988 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/BenchmarkResult.java @@ -0,0 +1,238 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the results of a single benchmark test. + * Fields match the Go implementation exactly for compatibility. + */ +public class BenchmarkResult { + + @JsonProperty("test_name") + private String testName; + + @JsonProperty("language") + private String language; + + @JsonProperty("data_size") + private int dataSize; + + @JsonProperty("concurrency") + private int concurrency; + + @JsonProperty("put_latency_ms") + private double putLatencyMs; + + @JsonProperty("get_latency_ms") + private double getLatencyMs; + + @JsonProperty("end_to_end_latency_ms") + private double endToEndLatencyMs; + + @JsonProperty("ops_per_second") + private double opsPerSecond; + + @JsonProperty("bytes_per_second") + private double bytesPerSecond; + + @JsonProperty("peak_memory_mb") + private double peakMemoryMb; + + @JsonProperty("memory_efficiency_ratio") + private double memoryEfficiencyRatio; + + @JsonProperty("p50_latency") + private double p50Latency; + + @JsonProperty("p95_latency") + private double p95Latency; + + @JsonProperty("p99_latency") + private double p99Latency; + + @JsonProperty("timestamp") + private String timestamp; + + @JsonProperty("java_version") + private String javaVersion; + + @JsonProperty("cpu_count") + private int cpuCount; + + @JsonProperty("total_memory_gb") + private double totalMemoryGb; + + // Constructors + public BenchmarkResult() {} + + public BenchmarkResult( + String testName, + String language, + int dataSize, + int concurrency + ) { + this.testName = testName; + this.language = language; + this.dataSize = dataSize; + this.concurrency = concurrency; + } + + // Getters and Setters + public String getTestName() { + return testName; + } + + public void setTestName(String testName) { + this.testName = testName; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public int getDataSize() { + return dataSize; + } + + public void setDataSize(int dataSize) { + this.dataSize = dataSize; + } + + public int getConcurrency() { + return concurrency; + } + + public void setConcurrency(int concurrency) { + this.concurrency = concurrency; + } + + public double getPutLatencyMs() { + return putLatencyMs; + } + + public void setPutLatencyMs(double putLatencyMs) { + this.putLatencyMs = putLatencyMs; + } + + public double getGetLatencyMs() { + return getLatencyMs; + } + + public void setGetLatencyMs(double getLatencyMs) { + this.getLatencyMs = getLatencyMs; + } + + public double getEndToEndLatencyMs() { + return endToEndLatencyMs; + } + + public void setEndToEndLatencyMs(double endToEndLatencyMs) { + this.endToEndLatencyMs = endToEndLatencyMs; + } + + public double getOpsPerSecond() { + return opsPerSecond; + } + + public void setOpsPerSecond(double opsPerSecond) { + this.opsPerSecond = opsPerSecond; + } + + public double getBytesPerSecond() { + return bytesPerSecond; + } + + public void setBytesPerSecond(double bytesPerSecond) { + this.bytesPerSecond = bytesPerSecond; + } + + public double getPeakMemoryMb() { + return peakMemoryMb; + } + + public void setPeakMemoryMb(double peakMemoryMb) { + this.peakMemoryMb = peakMemoryMb; + } + + public double getMemoryEfficiencyRatio() { + return memoryEfficiencyRatio; + } + + public void setMemoryEfficiencyRatio(double memoryEfficiencyRatio) { + this.memoryEfficiencyRatio = memoryEfficiencyRatio; + } + + public double getP50Latency() { + return p50Latency; + } + + public void setP50Latency(double p50Latency) { + this.p50Latency = p50Latency; + } + + public double getP95Latency() { + return p95Latency; + } + + public void setP95Latency(double p95Latency) { + this.p95Latency = p95Latency; + } + + public double getP99Latency() { + return p99Latency; + } + + public void setP99Latency(double p99Latency) { + this.p99Latency = p99Latency; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getJavaVersion() { + return javaVersion; + } + + public void setJavaVersion(String javaVersion) { + this.javaVersion = javaVersion; + } + + public int getCpuCount() { + return cpuCount; + } + + public void setCpuCount(int cpuCount) { + this.cpuCount = cpuCount; + } + + public double getTotalMemoryGb() { + return totalMemoryGb; + } + + public void setTotalMemoryGb(double totalMemoryGb) { + this.totalMemoryGb = totalMemoryGb; + } + + @Override + public String toString() { + return String.format( + "BenchmarkResult{testName='%s', dataSize=%d, concurrency=%d, opsPerSecond=%.2f}", + testName, + dataSize, + concurrency, + opsPerSecond + ); + } +} diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/DBESDKBenchmark.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/DBESDKBenchmark.java new file mode 100644 index 000000000..bfda75631 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/DBESDKBenchmark.java @@ -0,0 +1,846 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor; +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.SymmetricStaticProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.crypto.spec.SecretKeySpec; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.DynamoDbItemEncryptor; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DecryptItemInput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DecryptItemOutput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.DynamoDbItemEncryptorConfig; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.EncryptItemInput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.itemencryptor.model.EncryptItemOutput; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.LegacyOverride; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.LegacyPolicy; +import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.model.DBEAlgorithmSuiteId; + +/** + * Main DB-ESDK benchmark class that orchestrates performance testing. + */ +public class DBESDKBenchmark { + + private static final int MEMORY_TEST_ITERATIONS = 5; + private static final int SAMPLING_INTERVAL_MS = 1; + private static final int GC_SETTLE_TIME_MS = 5; + private static final int FINAL_SAMPLE_WAIT_MS = 2; + + private final TestConfig config; + private final IKeyring keyring; + private final DynamoDbItemEncryptor itemEncryptor; + private final List results; + private final int cpuCount; + private final double totalMemoryGb; + private final boolean useLegacyOverride; + + /** + * Creates a new benchmark instance. + * + * @param configPath Path to the YAML configuration file + * @throws Exception if initialization fails + */ + public DBESDKBenchmark(String configPath) throws Exception { + this(configPath, false); + } + + /** + * Creates a new benchmark instance with legacy override option. + * + * @param configPath Path to the YAML configuration file + * @param useLegacyOverride Whether to use legacy override for encryption/decryption + * @throws Exception if initialization fails + */ + public DBESDKBenchmark(String configPath, boolean useLegacyOverride) + throws Exception { + this.config = TestConfig.loadConfig(configPath); + this.results = new ArrayList<>(); + this.cpuCount = Utils.getCpuCount(); + this.totalMemoryGb = Utils.getTotalMemoryGb(); + this.useLegacyOverride = useLegacyOverride; + + // Initialize keyring + this.keyring = KeyringSetup.createKeyring(config.getKeyring()); + + // Initialize item encryptor + this.itemEncryptor = setupItemEncryptor(); + + System.out.printf( + "Initialized DB-ESDK Benchmark - CPU cores: %d, Memory: %.1f GB, Legacy override: %s%n", + cpuCount, + totalMemoryGb, + useLegacyOverride + ); + } + + /** + * Sets up the DynamoDB Item Encryptor with proper configuration. + */ + private DynamoDbItemEncryptor setupItemEncryptor() throws Exception { + // Define attribute actions (matching Go implementation) + Map attributeActions = new HashMap<>(); + attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); + attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); + attributeActions.put("attribute1", CryptoAction.ENCRYPT_AND_SIGN); + attributeActions.put("attribute2", CryptoAction.SIGN_ONLY); + attributeActions.put(":attribute3", CryptoAction.DO_NOTHING); + + String allowedUnsignedAttributePrefix = ":"; + DBEAlgorithmSuiteId algorithmSuiteId = + DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384; + + DynamoDbItemEncryptorConfig.Builder configBuilder = + DynamoDbItemEncryptorConfig + .builder() + .logicalTableName(config.getTableName()) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActions) + .keyring(keyring) + .allowedUnsignedAttributePrefix(allowedUnsignedAttributePrefix) + .algorithmSuiteId(algorithmSuiteId); + + // Add legacy override if requested + if (useLegacyOverride) { + LegacyOverride legacyOverride = createLegacyOverride(); + configBuilder.legacyOverride(legacyOverride); + } + + return DynamoDbItemEncryptor + .builder() + .DynamoDbItemEncryptorConfig(configBuilder.build()) + .build(); + } + + /** + * Creates a legacy override configuration for benchmarking legacy encryption clients. + */ + private LegacyOverride createLegacyOverride() throws Exception { + // Create legacy encryptor using SymmetricStaticProvider + DynamoDBEncryptor legacyEncryptor = createLegacyEncryptor(); + + // Define legacy attribute actions (matching current benchmark structure) + Map legacyActions = new HashMap<>(); + legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); + legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); + legacyActions.put("attribute1", CryptoAction.ENCRYPT_AND_SIGN); + legacyActions.put("attribute2", CryptoAction.SIGN_ONLY); + legacyActions.put(":attribute3", CryptoAction.DO_NOTHING); + + return LegacyOverride + .builder() + .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) + .encryptor(legacyEncryptor) + .attributeActionsOnEncrypt(legacyActions) + .build(); + } + + /** + * Creates a legacy DynamoDB encryptor using SymmetricStaticProvider. + */ + private DynamoDBEncryptor createLegacyEncryptor() throws Exception { + // Generate a 256-bit AES key for symmetric encryption + byte[] aesKey = new byte[32]; // 256 bits + new java.security.SecureRandom().nextBytes(aesKey); + + // Create AES key spec + SecretKeySpec encryptionKey = new SecretKeySpec(aesKey, "AES"); + + // Generate MAC key (can be same as encryption key for testing) + SecretKeySpec macKey = new SecretKeySpec(aesKey, "HmacSHA256"); + + // Create symmetric static provider + SymmetricStaticProvider provider = new SymmetricStaticProvider( + encryptionKey, + macKey + ); + + return DynamoDBEncryptor.getInstance(provider); + } + + /** + * Runs all configured benchmark tests. + */ + public List runAllBenchmarks() throws Exception { + System.out.println("Starting comprehensive DB-ESDK benchmark suite"); + + // Combine all data sizes + List allDataSizes = Stream + .of( + config.getDataSizes().getSmall(), + config.getDataSizes().getMedium(), + config.getDataSizes().getLarge() + ) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toList()); + + // Get allowed test types from quick config if available + List allowedTestTypes = config.getQuickConfig() != null + ? config.getQuickConfig().getTestTypes() + : null; + + // Run test suites + if (Utils.shouldRunTestType("throughput", allowedTestTypes)) { + runThroughputTests(allDataSizes, config.getIterations().getMeasurement()); + } else { + System.out.println("Skipping throughput tests (not in test_types)"); + } + + if (Utils.shouldRunTestType("memory", allowedTestTypes)) { + runMemoryTests(allDataSizes); + } else { + System.out.println("Skipping memory tests (not in test_types)"); + } + + if (Utils.shouldRunTestType("concurrency", allowedTestTypes)) { + runConcurrencyTests(allDataSizes, config.getConcurrencyLevels()); + } else { + System.out.println("Skipping concurrency tests (not in test_types)"); + } + + System.out.printf( + "Benchmark suite completed. Total results: %d%n", + results.size() + ); + return results; + } + + /** + * Runs throughput benchmark tests. + */ + private void runThroughputTests(List dataSizes, int iterations) + throws Exception { + System.out.println("Running throughput tests..."); + for (int dataSize : dataSizes) { + BenchmarkResult result = runThroughputTest(dataSize, iterations); + if (result != null) { + results.add(result); + System.out.printf( + "Throughput test completed: %.2f ops/sec%n", + result.getOpsPerSecond() + ); + } + } + } + + /** + * Runs a single throughput test. + */ + private BenchmarkResult runThroughputTest(int dataSize, int iterations) + throws Exception { + System.out.printf( + "Running throughput test - Size: %d bytes, Iterations: %d%n", + dataSize, + iterations + ); + + byte[] testData = Utils.generateTestData(dataSize); + + // Warmup + for (int i = 0; i < config.getIterations().getWarmup(); i++) { + runItemEncryptorCycle(testData); + } + + // Measurement runs + List putLatencies = new ArrayList<>(); + List getLatencies = new ArrayList<>(); + List endToEndLatencies = new ArrayList<>(); + long totalBytes = 0; + + long startTime = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + long iterationStart = System.nanoTime(); + + LatencyResult latencyResult = runItemEncryptorCycle(testData); + + long iterationDuration = System.nanoTime() - iterationStart; + double iterationMs = Utils.nanosToMillis(iterationDuration); + + putLatencies.add(latencyResult.putLatencyMs); + getLatencies.add(latencyResult.getLatencyMs); + endToEndLatencies.add(iterationMs); + totalBytes += dataSize; + } + double totalDuration = + Utils.nanosToMillis(System.nanoTime() - startTime) / 1000.0; // Convert to seconds + + // Calculate metrics + double[] endToEndArray = endToEndLatencies + .stream() + .mapToDouble(Double::doubleValue) + .toArray(); + Arrays.sort(endToEndArray); + + BenchmarkResult result = new BenchmarkResult( + "throughput", + useLegacyOverride ? "java-ddbec-native" : "java", + dataSize, + 1 + ); + result.setPutLatencyMs(Utils.average(putLatencies)); + result.setGetLatencyMs(Utils.average(getLatencies)); + result.setEndToEndLatencyMs(Utils.average(endToEndLatencies)); + result.setOpsPerSecond(iterations / totalDuration); + result.setBytesPerSecond(totalBytes / totalDuration); + result.setP50Latency(Utils.percentile(endToEndArray, 0.50)); + result.setP95Latency(Utils.percentile(endToEndArray, 0.95)); + result.setP99Latency(Utils.percentile(endToEndArray, 0.99)); + result.setTimestamp(Utils.getCurrentTimestamp()); + result.setJavaVersion(Utils.getJavaVersion()); + result.setCpuCount(cpuCount); + result.setTotalMemoryGb(totalMemoryGb); + + System.out.printf( + "Throughput test completed - Ops/sec: %.2f, MB/sec: %.2f%n", + result.getOpsPerSecond(), + result.getBytesPerSecond() / (1024 * 1024) + ); + + return result; + } + + /** + * Runs memory benchmark tests. + */ + private void runMemoryTests(List dataSizes) throws Exception { + System.out.println("Running memory tests..."); + for (int dataSize : dataSizes) { + BenchmarkResult result = runMemoryTest(dataSize); + if (result != null) { + results.add(result); + System.out.printf( + "Memory test completed: %.2f MB peak%n", + result.getPeakMemoryMb() + ); + } + } + } + + /** + * Gets the total allocated bytes for the current thread. + */ + private static long getTotalAllocatedBytes() { + final Object threadBean = ManagementFactory.getThreadMXBean(); + final com.sun.management.ThreadMXBean sunThreadBean = + (com.sun.management.ThreadMXBean) threadBean; + + if (!sunThreadBean.isThreadAllocatedMemoryEnabled()) { + sunThreadBean.setThreadAllocatedMemoryEnabled(true); + } + + return sunThreadBean.getCurrentThreadAllocatedBytes(); + } + + /** + * Runs a single memory test with enhanced continuous monitoring. + */ + private BenchmarkResult runMemoryTest(int dataSize) throws Exception { + System.out.printf( + "Running memory test - Size: %d bytes (%d iterations, continuous sampling)%n", + dataSize, + MEMORY_TEST_ITERATIONS + ); + System.out.flush(); + + byte[] data = Utils.generateTestData(dataSize); + MemoryResults memoryResults = sampleMemoryDuringOperations(data); + + if (memoryResults == null) { + throw new RuntimeException( + "Memory test failed: Unable to collect memory samples for data size " + + dataSize + + " bytes" + ); + } + + BenchmarkResult result = new BenchmarkResult( + "memory", + useLegacyOverride ? "java-ddbec-native" : "java", + dataSize, + 1 + ); + result.setPeakMemoryMb(memoryResults.peakMemoryMb); + result.setMemoryEfficiencyRatio( + memoryResults.peakMemoryMb > 0 + ? dataSize / (memoryResults.peakMemoryMb * 1024 * 1024) + : 0.0 + ); + result.setTimestamp(Utils.getCurrentTimestamp()); + result.setJavaVersion(Utils.getJavaVersion()); + result.setCpuCount(cpuCount); + result.setTotalMemoryGb(totalMemoryGb); + + return result; + } + + /** + * Samples memory usage during multiple test iterations. + */ + private MemoryResults sampleMemoryDuringOperations(byte[] data) { + double peakMemoryDelta = 0.0; + double peakAllocations = 0.0; + List avgMemoryValues = new ArrayList<>(); + + for (int i = 0; i < MEMORY_TEST_ITERATIONS; i++) { + IterationResult iterationResult = runSingleMemoryIteration(data, i + 1); + + if (iterationResult.peakMemory > peakMemoryDelta) { + peakMemoryDelta = iterationResult.peakMemory; + } + if (iterationResult.totalAllocs > peakAllocations) { + peakAllocations = iterationResult.totalAllocs; + } + avgMemoryValues.add(iterationResult.avgMemory); + } + + double overallAvgMemory = avgMemoryValues.isEmpty() + ? 0.0 + : avgMemoryValues + .stream() + .mapToDouble(Double::doubleValue) + .average() + .orElse(0.0); + + System.out.println("\nMemory Summary:"); + System.out.printf( + "- Absolute Peak Heap: %.2f MB (across all runs)%n", + peakMemoryDelta + ); + System.out.printf( + "- Average Heap: %.2f MB (across all runs)%n", + overallAvgMemory + ); + System.out.printf( + "- Total Allocations: %.2f MB (max across all runs)%n", + peakAllocations + ); + System.out.flush(); + + return new MemoryResults(peakMemoryDelta, overallAvgMemory); + } + + /** + * Runs a single memory iteration with enhanced sampling. + */ + private IterationResult runSingleMemoryIteration(byte[] data, int iteration) { + // Force GC and settle (matching ESDK approach) + System.gc(); + System.gc(); + try { + Thread.sleep(GC_SETTLE_TIME_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException( + "Memory test interrupted during GC settle phase", + e + ); + } + + long baselineMemory = + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + long baselineAllocations = getTotalAllocatedBytes(); + List memorySamples = new ArrayList<>(); + + long operationStart = System.nanoTime(); + + // Start background sampling + Thread samplingTask = new Thread(() -> { + try { + while (System.nanoTime() - operationStart < 100_000_000) { // 100ms max + long currentMemory = + Runtime.getRuntime().totalMemory() - + Runtime.getRuntime().freeMemory(); + long currentAllocations = getTotalAllocatedBytes(); + double heapDelta = + (currentMemory - baselineMemory) / (1024.0 * 1024.0); + double cumulativeAllocs = + (currentAllocations - baselineAllocations) / (1024.0 * 1024.0); + + if (heapDelta > 0 || cumulativeAllocs > 0) { + synchronized (memorySamples) { + memorySamples.add( + new EnhancedMemorySample( + Math.max(0, heapDelta), + Math.max(0, cumulativeAllocs) + ) + ); + } + } + Thread.sleep(SAMPLING_INTERVAL_MS); + } + } catch (InterruptedException e) { + // Expected when operation completes + } + }); + + samplingTask.start(); + + // Run the actual operation + try { + runItemEncryptorCycle(data); + } catch (Exception e) { + System.out.printf( + "Memory test iteration %d failed: %s%n", + iteration, + e.getMessage() + ); + return new IterationResult(0.0, 0.0, 0.0); + } + + double operationDurationMs = + (System.nanoTime() - operationStart) / 1_000_000.0; + + // Wait for sampling to complete + try { + Thread.sleep(FINAL_SAMPLE_WAIT_MS); + samplingTask.join(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return calculateIterationMetrics( + baselineMemory, + baselineAllocations, + memorySamples, + iteration, + operationDurationMs + ); + } + + /** + * Calculates comprehensive metrics for a single iteration. + */ + private IterationResult calculateIterationMetrics( + long baselineMemory, + long baselineAllocations, + List memorySamples, + int iteration, + double operationDurationMs + ) { + long finalMemory = + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + long finalAllocations = getTotalAllocatedBytes(); + double finalHeapDelta = (finalMemory - baselineMemory) / (1024.0 * 1024.0); + double finalCumulativeAllocs = + (finalAllocations - baselineAllocations) / (1024.0 * 1024.0); + + double iterPeakMemory; + double iterTotalAllocs; + double iterAvgMemory; + + synchronized (memorySamples) { + if (memorySamples.isEmpty()) { + iterPeakMemory = Math.max(0, finalHeapDelta); + iterTotalAllocs = Math.max(0, finalCumulativeAllocs); + iterAvgMemory = Math.max(0, finalHeapDelta); + } else { + iterPeakMemory = + memorySamples.stream().mapToDouble(s -> s.heapMB).max().orElse(0.0); + iterTotalAllocs = Math.max(0, finalCumulativeAllocs); + iterAvgMemory = + memorySamples + .stream() + .mapToDouble(s -> s.heapMB) + .average() + .orElse(0.0); + } + } + + System.out.printf( + "=== Iteration %d === Peak Heap: %.2f MB, Total Allocs: %.2f MB, " + + "Avg Heap: %.2f MB (%.0fms, %d samples)%n", + iteration, + iterPeakMemory, + iterTotalAllocs, + iterAvgMemory, + operationDurationMs, + memorySamples.size() + ); + System.out.flush(); + + return new IterationResult(iterPeakMemory, iterTotalAllocs, iterAvgMemory); + } + + /** + * Runs concurrency benchmark tests. + */ + private void runConcurrencyTests( + List dataSizes, + List concurrencyLevels + ) throws Exception { + System.out.println("Running concurrency tests..."); + for (int dataSize : dataSizes) { + for (int concurrency : concurrencyLevels) { + if (concurrency > 1) { // Skip single-threaded + BenchmarkResult result = runConcurrentTest(dataSize, concurrency, 5); + if (result != null) { + results.add(result); + System.out.printf( + "Concurrent test completed: %.2f ops/sec @ %d threads%n", + result.getOpsPerSecond(), + concurrency + ); + } + } + } + } + } + + /** + * Runs a single concurrent test. + */ + private BenchmarkResult runConcurrentTest( + int dataSize, + int concurrency, + int iterationsPerWorker + ) throws Exception { + System.out.printf( + "Running concurrent test - Size: %d bytes, Concurrency: %d%n", + dataSize, + concurrency + ); + + byte[] data = Utils.generateTestData(dataSize); + List allTimes = Collections.synchronizedList(new ArrayList<>()); + ExecutorService executor = Executors.newFixedThreadPool(concurrency); + + long startTime = System.nanoTime(); + + try { + // Submit workers + List> futures = new ArrayList<>(); + for (int i = 0; i < concurrency; i++) { + final int workerId = i; + CompletableFuture future = CompletableFuture.runAsync( + () -> { + for (int j = 0; j < iterationsPerWorker; j++) { + try { + long iterStart = System.nanoTime(); + runItemEncryptorCycle(data); + double iterMs = Utils.nanosToMillis( + System.nanoTime() - iterStart + ); + allTimes.add(iterMs); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Worker %d iteration %d failed: %s", + workerId, + j, + e.getMessage() + ), + e + ); + } + } + }, + executor + ); + futures.add(future); + } + + // Wait for all workers to complete + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); + } finally { + executor.shutdown(); + } + + double totalDurationSec = + Utils.nanosToMillis(System.nanoTime() - startTime) / 1000.0; + + // Calculate metrics + int totalOps = concurrency * iterationsPerWorker; + long totalBytes = (long) totalOps * dataSize; + + double[] timesArray = allTimes + .stream() + .mapToDouble(Double::doubleValue) + .toArray(); + Arrays.sort(timesArray); + + BenchmarkResult result = new BenchmarkResult( + "concurrent", + useLegacyOverride ? "java-ddbec-native" : "java", + dataSize, + concurrency + ); + result.setEndToEndLatencyMs(Utils.average(timesArray)); + result.setOpsPerSecond(totalOps / totalDurationSec); + result.setBytesPerSecond(totalBytes / totalDurationSec); + result.setP50Latency(Utils.percentile(timesArray, 0.50)); + result.setP95Latency(Utils.percentile(timesArray, 0.95)); + result.setP99Latency(Utils.percentile(timesArray, 0.99)); + result.setTimestamp(Utils.getCurrentTimestamp()); + result.setJavaVersion(Utils.getJavaVersion()); + result.setCpuCount(cpuCount); + result.setTotalMemoryGb(totalMemoryGb); + + System.out.printf( + "Concurrent test completed - Ops/sec: %.2f, Avg latency: %.2f ms%n", + result.getOpsPerSecond(), + result.getEndToEndLatencyMs() + ); + + return result; + } + + /** + * Performs a single encrypt-decrypt cycle and measures performance. + */ + private LatencyResult runItemEncryptorCycle(byte[] data) throws Exception { + // Create DynamoDB item + Map item = new HashMap<>(); + item.put( + "partition_key", + AttributeValue.builder().s("benchmark-test").build() + ); + item.put("sort_key", AttributeValue.builder().n("0").build()); + + Map nestedMap = new HashMap<>(); + nestedMap.put( + "data", + AttributeValue.builder().b(SdkBytes.fromByteArray(data)).build() + ); + item.put("attribute1", AttributeValue.builder().m(nestedMap).build()); + + item.put("attribute2", AttributeValue.builder().s("sign me!").build()); + item.put(":attribute3", AttributeValue.builder().s("ignore me!").build()); + + // Encrypt item + long encryptStart = System.nanoTime(); + EncryptItemInput encryptInput = EncryptItemInput + .builder() + .plaintextItem(item) + .build(); + EncryptItemOutput encryptOutput = itemEncryptor.EncryptItem(encryptInput); + double putLatencyMs = Utils.nanosToMillis(System.nanoTime() - encryptStart); + + Map encryptedItem = encryptOutput.encryptedItem(); + + // Decrypt item + long decryptStart = System.nanoTime(); + DecryptItemInput decryptInput = DecryptItemInput + .builder() + .encryptedItem(encryptedItem) + .build(); + DecryptItemOutput decryptOutput = itemEncryptor.DecryptItem(decryptInput); + double getLatencyMs = Utils.nanosToMillis(System.nanoTime() - decryptStart); + + Map decryptedItem = decryptOutput.plaintextItem(); + + // Verify items match (basic check) + if (!item.equals(decryptedItem)) { + throw new RuntimeException("Decrypted item does not match original item"); + } + + return new LatencyResult(putLatencyMs, getLatencyMs); + } + + /** + * Saves benchmark results to JSON file. + */ + public void saveResults(String outputPath) throws IOException { + // Ensure output directory exists + Path outputFilePath = Paths.get(outputPath); + Files.createDirectories(outputFilePath.getParent()); + + // Create results structure matching Go implementation + Map resultsData = new HashMap<>(); + + Map metadata = new HashMap<>(); + metadata.put("language", "java"); + metadata.put("timestamp", Utils.getCurrentTimestamp()); + metadata.put("java_version", Utils.getJavaVersion()); + metadata.put("cpu_count", cpuCount); + metadata.put("total_memory_gb", totalMemoryGb); + metadata.put("total_tests", results.size()); + + resultsData.put("metadata", metadata); + resultsData.put("results", results); + + // Write to JSON file with proper formatting + ObjectMapper mapper = new ObjectMapper(); + mapper + .writerWithDefaultPrettyPrinter() + .writeValue(new File(outputPath), resultsData); + + System.out.printf("Results saved to: %s%n", outputPath); + } + + // Getters + public TestConfig getConfig() { + return config; + } + + public List getResults() { + return results; + } + + /** + * Helper class to hold latency results. + */ + private static class LatencyResult { + + final double putLatencyMs; + final double getLatencyMs; + + LatencyResult(double putLatencyMs, double getLatencyMs) { + this.putLatencyMs = putLatencyMs; + this.getLatencyMs = getLatencyMs; + } + } + + // Helper classes for enhanced memory testing + private static class IterationResult { + + final double peakMemory; + final double totalAllocs; + final double avgMemory; + + IterationResult(double peakMemory, double totalAllocs, double avgMemory) { + this.peakMemory = peakMemory; + this.totalAllocs = totalAllocs; + this.avgMemory = avgMemory; + } + } + + private static class MemoryResults { + + final double peakMemoryMb; + final double avgMemoryMb; + + MemoryResults(double peakMemoryMb, double avgMemoryMb) { + this.peakMemoryMb = peakMemoryMb; + this.avgMemoryMb = avgMemoryMb; + } + } + + private static class EnhancedMemorySample { + + final double heapMB; + final double allocsMB; + + EnhancedMemorySample(double heapMB, double allocsMB) { + this.heapMB = heapMB; + this.allocsMB = allocsMB; + } + } +} diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/KeyringSetup.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/KeyringSetup.java new file mode 100644 index 000000000..3fe7b6c42 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/KeyringSetup.java @@ -0,0 +1,69 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Map; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.AesWrappingAlg; +import software.amazon.cryptography.materialproviders.model.CreateRawAesKeyringInput; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; + +/** + * Helper class for setting up keyrings for DB-ESDK benchmarks. + */ +public class KeyringSetup { + + private static final String RAW_AES_KEYRING = "raw-aes"; + + /** + * Creates a keyring based on the specified type. + * + * @param keyringType The type of keyring to create + * @return The configured keyring + * @throws Exception if keyring creation fails + */ + public static IKeyring createKeyring(String keyringType) throws Exception { + if (RAW_AES_KEYRING.equals(keyringType)) { + return createRawAesKeyring(); + } else { + throw new IllegalArgumentException( + "Unsupported keyring type: " + keyringType + ); + } + } + + /** + * Creates a Raw AES keyring with a randomly generated 256-bit key. + * This matches the Go implementation's keyring setup. + * + * @return Raw AES keyring + * @throws Exception if keyring creation fails + */ + private static IKeyring createRawAesKeyring() throws Exception { + // Initialize Material Providers client + MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + + // Generate a 256-bit (32-byte) random key + byte[] key = new byte[32]; + new SecureRandom().nextBytes(key); + + // Create Raw AES keyring input + CreateRawAesKeyringInput keyringInput = CreateRawAesKeyringInput + .builder() + .keyName("test-aes-256-key") + .keyNamespace("DB-ESDK-performance-test") + .wrappingKey(ByteBuffer.wrap(key)) + .wrappingAlg(AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16) + .build(); + + // Create and return the keyring + return matProv.CreateRawAesKeyring(keyringInput); + } +} diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Program.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Program.java new file mode 100644 index 000000000..9850a000f --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Program.java @@ -0,0 +1,323 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import java.io.IOException; +import java.util.List; +import org.apache.commons.cli.*; + +/** + * Main entry point for the DB-ESDK Java performance benchmark. + */ +public class Program { + + public static void main(String[] args) { + CommandLineOptions options = parseArgs(args); + if (options == null) { + return; + } + + try { + // Initialize benchmark + DBESDKBenchmark benchmark = new DBESDKBenchmark( + options.getConfigPath(), + options.isLegacyOverride() + ); + + // Adjust config for quick test if requested + if (options.isQuickTest()) { + benchmark.getConfig().adjustForQuickTest(); + } + + System.out.println("\n=== Starting DB-ESDK Java Benchmark ==="); + System.out.printf("Configuration: %s%n", options.getConfigPath()); + System.out.printf("Output: %s%n", options.getOutputPath()); + System.out.printf("Quick mode: %s%n", options.isQuickTest()); + System.out.printf( + "System: %d CPU cores, %.1f GB memory%n", + Utils.getCpuCount(), + Utils.getTotalMemoryGb() + ); + + // Run benchmarks + List results = benchmark.runAllBenchmarks(); + + // Save results + benchmark.saveResults(options.getOutputPath()); + + // Print summary + printSummary(results, options.getOutputPath()); + } catch (Exception e) { + System.err.println("Benchmark failed: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + /** + * Parses command line arguments. + */ + private static CommandLineOptions parseArgs(String[] args) { + Options options = new Options(); + + options.addOption( + Option + .builder("c") + .longOpt("config") + .hasArg() + .argName("file") + .desc( + "Path to test configuration file (default: ../config/test-scenarios.yaml)" + ) + .build() + ); + + options.addOption( + Option + .builder("o") + .longOpt("output") + .hasArg() + .argName("file") + .desc( + "Path to output results file (default: ../results/raw-data/java_results.json)" + ) + .build() + ); + + options.addOption( + Option + .builder("q") + .longOpt("quick") + .desc("Run quick test with reduced iterations") + .build() + ); + + options.addOption( + Option + .builder("l") + .longOpt("legacy-override") + .desc( + "Use legacy DynamoDB Encryption Client (v2.x) for encryption/decryption" + ) + .build() + ); + + options.addOption( + Option.builder("h").longOpt("help").desc("Show this help message").build() + ); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine cmd = parser.parse(options, args); + + if (cmd.hasOption("h")) { + printUsage(options); + return null; + } + + String configPath = cmd.getOptionValue( + "c", + "../config/test-scenarios.yaml" + ); + String outputPath = cmd.getOptionValue( + "o", + "../results/raw-data/java_results.json" + ); + boolean quickTest = cmd.hasOption("q"); + boolean legacyOverride = cmd.hasOption("l"); + + if ( + legacyOverride && + outputPath.equals("../results/raw-data/java_results.json") + ) { + outputPath = "../results/raw-data/java_ddbec_results.json"; + } + + return new CommandLineOptions( + configPath, + outputPath, + quickTest, + legacyOverride + ); + } catch (ParseException e) { + System.err.println("Error parsing command line: " + e.getMessage()); + printUsage(options); + return null; + } + } + + /** + * Prints usage information. + */ + private static void printUsage(Options options) { + HelpFormatter formatter = new HelpFormatter(); + System.out.println("DB-ESDK Java Performance Benchmark"); + System.out.println(); + formatter.printHelp("java -jar db-esdk-benchmark.jar [options]", options); + System.out.println(); + System.out.println("Examples:"); + System.out.println(" # Run quick benchmark"); + System.out.println(" java -jar db-esdk-benchmark.jar --quick"); + System.out.println(); + System.out.println(" # Run full benchmark with custom config"); + System.out.println( + " java -jar db-esdk-benchmark.jar -c /path/to/config.yaml -o /path/to/results.json" + ); + System.out.println(); + System.out.println(" # Run legacy override benchmark"); + System.out.println(" java -jar db-esdk-benchmark.jar --legacy-override"); + System.out.println(); + System.out.println(" # Using Gradle"); + System.out.println(" ./gradlew run --args=\"--quick\""); + } + + /** + * Prints benchmark summary. + */ + private static void printSummary( + List results, + String outputPath + ) { + System.out.println("\n=== DB-ESDK Java Benchmark Summary ==="); + System.out.printf("Total tests completed: %d%n", results.size()); + System.out.printf("Results saved to: %s%n", outputPath); + + if (!results.isEmpty()) { + // Find maximum throughput + double maxThroughput = results + .stream() + .filter(r -> "throughput".equals(r.getTestName())) + .mapToDouble(BenchmarkResult::getOpsPerSecond) + .max() + .orElse(0.0); + + if (maxThroughput > 0) { + System.out.printf("Maximum throughput: %.2f ops/sec%n", maxThroughput); + } + + // Print some sample results by test type + System.out.println(); + printTestTypeSummary(results, "throughput"); + printTestTypeSummary(results, "memory"); + printTestTypeSummary(results, "concurrent"); + } + + System.out.println("\n=== Benchmark Complete ==="); + } + + /** + * Prints summary for a specific test type. + */ + private static void printTestTypeSummary( + List results, + String testType + ) { + List testResults = results + .stream() + .filter(r -> testType.equals(r.getTestName())) + .collect(java.util.stream.Collectors.toList()); + + if (testResults.isEmpty()) { + return; + } + + System.out.printf( + "\n%s Tests (%d results):%n", + testType.substring(0, 1).toUpperCase() + testType.substring(1), + testResults.size() + ); + + if ("throughput".equals(testType)) { + testResults + .stream() + .limit(3) // Show first 3 results + .forEach(r -> + System.out.printf( + " %s: %.2f ops/sec, %.2f ms latency%n", + formatDataSize(r.getDataSize()), + r.getOpsPerSecond(), + r.getEndToEndLatencyMs() + ) + ); + } else if ("memory".equals(testType)) { + testResults + .stream() + .limit(3) + .forEach(r -> + System.out.printf( + " %s: %.2f MB peak, %.4f efficiency%n", + formatDataSize(r.getDataSize()), + r.getPeakMemoryMb(), + r.getMemoryEfficiencyRatio() + ) + ); + } else if ("concurrent".equals(testType)) { + testResults + .stream() + .limit(3) + .forEach(r -> + System.out.printf( + " %s (%d threads): %.2f ops/sec%n", + formatDataSize(r.getDataSize()), + r.getConcurrency(), + r.getOpsPerSecond() + ) + ); + } + } + + /** + * Formats data size for display. + */ + private static String formatDataSize(int bytes) { + if (bytes >= 1024 * 1024 * 1024) { + return String.format("%.0f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } else if (bytes >= 1024 * 1024) { + return String.format("%.0f MB", bytes / (1024.0 * 1024.0)); + } else if (bytes >= 1024) { + return String.format("%.0f KB", bytes / 1024.0); + } else { + return bytes + " B"; + } + } + + /** + * Command line options holder. + */ + public static class CommandLineOptions { + + private final String configPath; + private final String outputPath; + private final boolean quickTest; + private final boolean legacyOverride; + + public CommandLineOptions( + String configPath, + String outputPath, + boolean quickTest, + boolean legacyOverride + ) { + this.configPath = configPath; + this.outputPath = outputPath; + this.quickTest = quickTest; + this.legacyOverride = legacyOverride; + } + + public String getConfigPath() { + return configPath; + } + + public String getOutputPath() { + return outputPath; + } + + public boolean isQuickTest() { + return quickTest; + } + + public boolean isLegacyOverride() { + return legacyOverride; + } + } +} diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/TestConfig.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/TestConfig.java new file mode 100644 index 000000000..5caa9383b --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/TestConfig.java @@ -0,0 +1,297 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.yaml.snakeyaml.Yaml; + +/** + * Configuration class for DB-ESDK benchmark tests, loaded from YAML. + */ +public class TestConfig { + + private DataSizes dataSizes; + private Iterations iterations; + private List concurrencyLevels; + private QuickConfig quickConfig; + private String tableName; + private String keyring; + + // Nested classes for structured configuration + public static class DataSizes { + + private List small; + private List medium; + private List large; + + public List getSmall() { + return small; + } + + public void setSmall(List small) { + this.small = small; + } + + public List getMedium() { + return medium; + } + + public void setMedium(List medium) { + this.medium = medium; + } + + public List getLarge() { + return large; + } + + public void setLarge(List large) { + this.large = large; + } + } + + public static class Iterations { + + private int warmup; + private int measurement; + + public int getWarmup() { + return warmup; + } + + public void setWarmup(int warmup) { + this.warmup = warmup; + } + + public int getMeasurement() { + return measurement; + } + + public void setMeasurement(int measurement) { + this.measurement = measurement; + } + } + + public static class QuickConfig { + + private DataSizes dataSizes; + private Iterations iterations; + private List concurrencyLevels; + private List testTypes; + + public DataSizes getDataSizes() { + return dataSizes; + } + + public void setDataSizes(DataSizes dataSizes) { + this.dataSizes = dataSizes; + } + + public Iterations getIterations() { + return iterations; + } + + public void setIterations(Iterations iterations) { + this.iterations = iterations; + } + + public List getConcurrencyLevels() { + return concurrencyLevels; + } + + public void setConcurrencyLevels(List concurrencyLevels) { + this.concurrencyLevels = concurrencyLevels; + } + + public List getTestTypes() { + return testTypes; + } + + public void setTestTypes(List testTypes) { + this.testTypes = testTypes; + } + } + + // Main getters and setters + public DataSizes getDataSizes() { + return dataSizes; + } + + public void setDataSizes(DataSizes dataSizes) { + this.dataSizes = dataSizes; + } + + public Iterations getIterations() { + return iterations; + } + + public void setIterations(Iterations iterations) { + this.iterations = iterations; + } + + public List getConcurrencyLevels() { + return concurrencyLevels; + } + + public void setConcurrencyLevels(List concurrencyLevels) { + this.concurrencyLevels = concurrencyLevels; + } + + public QuickConfig getQuickConfig() { + return quickConfig; + } + + public void setQuickConfig(QuickConfig quickConfig) { + this.quickConfig = quickConfig; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getKeyring() { + return keyring; + } + + public void setKeyring(String keyring) { + this.keyring = keyring; + } + + /** + * Loads test configuration from YAML file. + */ + public static TestConfig loadConfig(String configPath) throws IOException { + Yaml yaml = new Yaml(); + try (FileInputStream fis = new FileInputStream(configPath)) { + Map yamlData = yaml.load(fis); + return parseYamlToConfig(yamlData); + } + } + + /** + * Parses the YAML data structure into TestConfig object. + */ + @SuppressWarnings("unchecked") + private static TestConfig parseYamlToConfig(Map yamlData) { + TestConfig config = new TestConfig(); + + // Parse data_sizes + if (yamlData.containsKey("data_sizes")) { + Map dataSizesMap = (Map) yamlData.get( + "data_sizes" + ); + DataSizes dataSizes = new DataSizes(); + dataSizes.setSmall((List) dataSizesMap.get("small")); + dataSizes.setMedium((List) dataSizesMap.get("medium")); + dataSizes.setLarge((List) dataSizesMap.get("large")); + config.setDataSizes(dataSizes); + } + + // Parse iterations + if (yamlData.containsKey("iterations")) { + Map iterationsMap = (Map) yamlData.get( + "iterations" + ); + Iterations iterations = new Iterations(); + iterations.setWarmup((Integer) iterationsMap.get("warmup")); + iterations.setMeasurement((Integer) iterationsMap.get("measurement")); + config.setIterations(iterations); + } + + // Parse concurrency_levels + if (yamlData.containsKey("concurrency_levels")) { + config.setConcurrencyLevels( + (List) yamlData.get("concurrency_levels") + ); + } + + // Parse quick_config + if (yamlData.containsKey("quick_config")) { + Map quickConfigMap = (Map) yamlData.get( + "quick_config" + ); + QuickConfig quickConfig = new QuickConfig(); + + // Parse quick_config data_sizes + if (quickConfigMap.containsKey("data_sizes")) { + Map quickDataSizesMap = (Map< + String, + Object + >) quickConfigMap.get("data_sizes"); + DataSizes quickDataSizes = new DataSizes(); + quickDataSizes.setSmall((List) quickDataSizesMap.get("small")); + quickConfig.setDataSizes(quickDataSizes); + } + + // Parse quick_config iterations + if (quickConfigMap.containsKey("iterations")) { + Map quickIterationsMap = (Map< + String, + Object + >) quickConfigMap.get("iterations"); + Iterations quickIterations = new Iterations(); + quickIterations.setWarmup((Integer) quickIterationsMap.get("warmup")); + quickIterations.setMeasurement( + (Integer) quickIterationsMap.get("measurement") + ); + quickConfig.setIterations(quickIterations); + } + + // Parse quick_config concurrency_levels and test_types + if (quickConfigMap.containsKey("concurrency_levels")) { + quickConfig.setConcurrencyLevels( + (List) quickConfigMap.get("concurrency_levels") + ); + } + if (quickConfigMap.containsKey("test_types")) { + quickConfig.setTestTypes( + (List) quickConfigMap.get("test_types") + ); + } + + config.setQuickConfig(quickConfig); + } + + // Parse table_name and keyring + config.setTableName((String) yamlData.get("table_name")); + config.setKeyring((String) yamlData.get("keyring")); + + return config; + } + + /** + * Adjusts configuration for quick test mode. + */ + public void adjustForQuickTest() { + if (quickConfig == null) { + throw new IllegalStateException( + "Quick mode requested but no quick_config found in config file" + ); + } + + // Apply quick config settings + this.iterations.setMeasurement( + quickConfig.getIterations().getMeasurement() + ); + this.iterations.setWarmup(quickConfig.getIterations().getWarmup()); + + // Replace data sizes with quick config + if (quickConfig.getDataSizes() != null) { + this.dataSizes.setSmall(quickConfig.getDataSizes().getSmall()); + this.dataSizes.setMedium(java.util.Collections.emptyList()); // Empty list + this.dataSizes.setLarge(java.util.Collections.emptyList()); // Empty list + } + + // Replace concurrency levels + if (quickConfig.getConcurrencyLevels() != null) { + this.concurrencyLevels = quickConfig.getConcurrencyLevels(); + } + } +} diff --git a/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Utils.java b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Utils.java new file mode 100644 index 000000000..95ac7316b --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/java/src/main/java/com/amazon/dbesdk/benchmark/Utils.java @@ -0,0 +1,224 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazon.dbesdk.benchmark; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +/** + * Utility functions for benchmark operations. + */ +public class Utils { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final DateTimeFormatter TIMESTAMP_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * Calculates the average of a list of double values. + */ + public static double average(List values) { + if (values.isEmpty()) { + return 0.0; + } + return values + .stream() + .mapToDouble(Double::doubleValue) + .average() + .orElse(0.0); + } + + /** + * Calculates the average of an array of double values. + */ + public static double average(double[] values) { + if (values.length == 0) { + return 0.0; + } + return Arrays.stream(values).average().orElse(0.0); + } + + /** + * Calculates the specified percentile from a sorted array of values. + * + * @param sortedValues Sorted array of values + * @param percentile Percentile to calculate (0.0 to 1.0) + * @return The percentile value + */ + public static double percentile(double[] sortedValues, double percentile) { + if (sortedValues.length == 0) { + return 0.0; + } + if (percentile <= 0.0) { + return sortedValues[0]; + } + if (percentile >= 1.0) { + return sortedValues[sortedValues.length - 1]; + } + + double index = percentile * (sortedValues.length - 1); + int lower = (int) Math.floor(index); + int upper = (int) Math.ceil(index); + + if (lower == upper) { + return sortedValues[lower]; + } + + double weight = index - lower; + return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight; + } + + /** + * Generates random test data of specified size. + * + * @param size Size in bytes + * @return Random byte array + */ + public static byte[] generateTestData(int size) { + byte[] data = new byte[size]; + RANDOM.nextBytes(data); + return data; + } + + /** + * Gets the current timestamp in the format used by the Go implementation. + */ + public static String getCurrentTimestamp() { + return LocalDateTime.now().format(TIMESTAMP_FORMAT); + } + + /** + * Gets the Java version string. + */ + public static String getJavaVersion() { + return System.getProperty("java.version"); + } + + /** + * Gets the number of available CPU cores. + */ + public static int getCpuCount() { + return Runtime.getRuntime().availableProcessors(); + } + + /** + * Gets the total system memory in GB. + */ + public static double getTotalMemoryGb() { + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long maxMemory = memoryMXBean.getHeapMemoryUsage().getMax(); + if (maxMemory == -1) { + // If max heap is not set, use the current committed memory as approximation + maxMemory = memoryMXBean.getHeapMemoryUsage().getCommitted(); + } + return maxMemory / (1024.0 * 1024.0 * 1024.0); + } + + /** + * Gets the current memory usage in MB. + */ + public static double getCurrentMemoryUsageMb() { + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long usedMemory = memoryMXBean.getHeapMemoryUsage().getUsed(); + return usedMemory / (1024.0 * 1024.0); + } + + /** + * Forces garbage collection and waits a short time for it to settle. + */ + public static void forceGcAndWait() { + System.gc(); + try { + Thread.sleep(5); // 5ms settle time, matching Go's GCSettleTimeMs + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Converts milliseconds to seconds. + */ + public static double msToSeconds(double milliseconds) { + return milliseconds / 1000.0; + } + + /** + * Converts nanoseconds to milliseconds. + */ + public static double nanosToMillis(long nanoseconds) { + return nanoseconds / 1_000_000.0; + } + + /** + * Checks if a test type should be run based on quick config test types. + * + * @param testType The test type to check + * @param allowedTypes List of allowed test types (null means all are allowed) + * @return true if the test should run + */ + public static boolean shouldRunTestType( + String testType, + List allowedTypes + ) { + if (allowedTypes == null || allowedTypes.isEmpty()) { + return true; + } + return allowedTypes.contains(testType); + } + + /** + * Formats a double value to 2 decimal places. + */ + public static String formatDouble(double value) { + return String.format("%.2f", value); + } + + /** + * Formats bytes per second to a human-readable format. + */ + public static String formatBytesPerSecond(double bytesPerSecond) { + if (bytesPerSecond >= 1_000_000_000) { + return String.format("%.2f GB/sec", bytesPerSecond / 1_000_000_000); + } else if (bytesPerSecond >= 1_000_000) { + return String.format("%.2f MB/sec", bytesPerSecond / 1_000_000); + } else if (bytesPerSecond >= 1_000) { + return String.format("%.2f KB/sec", bytesPerSecond / 1_000); + } else { + return String.format("%.2f B/sec", bytesPerSecond); + } + } + + /** + * Memory sample class for continuous memory monitoring. + */ + public static class MemorySample { + + private final long timestamp; + private final double heapMb; + private final double allocatedMb; + + public MemorySample(long timestamp, double heapMb, double allocatedMb) { + this.timestamp = timestamp; + this.heapMb = heapMb; + this.allocatedMb = allocatedMb; + } + + public long getTimestamp() { + return timestamp; + } + + public double getHeapMb() { + return heapMb; + } + + public double getAllocatedMb() { + return allocatedMb; + } + } +} diff --git a/db-esdk-performance-testing/benchmarks/results/raw-data/java_ddbec_results.json b/db-esdk-performance-testing/benchmarks/results/raw-data/java_ddbec_results.json new file mode 100644 index 000000000..1a29302f2 --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/results/raw-data/java_ddbec_results.json @@ -0,0 +1,72 @@ +{ + "metadata": { + "total_memory_gb": 10.6669921875, + "java_version": "1.8.0_482", + "language": "java", + "total_tests": 3, + "timestamp": "2026-01-21 16:23:20", + "cpu_count": 14 + }, + "results": [ + { + "test_name": "throughput", + "language": "java-ddbec-native", + "data_size": 102400, + "concurrency": 1, + "put_latency_ms": 2.2290973333333333, + "get_latency_ms": 1.6495416666666667, + "end_to_end_latency_ms": 3.966680333333333, + "ops_per_second": 251.9720381589814, + "bytes_per_second": 2.5801936707479693e7, + "peak_memory_mb": 0.0, + "memory_efficiency_ratio": 0.0, + "p50_latency": 3.805291, + "p95_latency": 4.3465663, + "p99_latency": 4.39467966, + "timestamp": "2026-01-21 16:23:19", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + }, + { + "test_name": "memory", + "language": "java-ddbec-native", + "data_size": 102400, + "concurrency": 1, + "put_latency_ms": 0.0, + "get_latency_ms": 0.0, + "end_to_end_latency_ms": 0.0, + "ops_per_second": 0.0, + "bytes_per_second": 0.0, + "peak_memory_mb": 9.600547790527344, + "memory_efficiency_ratio": 0.010171945615057023, + "p50_latency": 0.0, + "p95_latency": 0.0, + "p99_latency": 0.0, + "timestamp": "2026-01-21 16:23:20", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + }, + { + "test_name": "concurrent", + "language": "java-ddbec-native", + "data_size": 102400, + "concurrency": 2, + "put_latency_ms": 0.0, + "get_latency_ms": 0.0, + "end_to_end_latency_ms": 5.1141749, + "ops_per_second": 348.0394502716883, + "bytes_per_second": 3.563923970782088e7, + "peak_memory_mb": 0.0, + "memory_efficiency_ratio": 0.0, + "p50_latency": 5.042729, + "p95_latency": 7.205733299999999, + "p99_latency": 7.51881306, + "timestamp": "2026-01-21 16:23:20", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + } + ] +} diff --git a/db-esdk-performance-testing/benchmarks/results/raw-data/java_results.json b/db-esdk-performance-testing/benchmarks/results/raw-data/java_results.json new file mode 100644 index 000000000..2142353ae --- /dev/null +++ b/db-esdk-performance-testing/benchmarks/results/raw-data/java_results.json @@ -0,0 +1,72 @@ +{ + "metadata": { + "total_memory_gb": 10.6669921875, + "java_version": "1.8.0_482", + "language": "java", + "total_tests": 3, + "timestamp": "2026-01-21 16:22:52", + "cpu_count": 14 + }, + "results": [ + { + "test_name": "throughput", + "language": "java", + "data_size": 102400, + "concurrency": 1, + "put_latency_ms": 6.358222333333334, + "get_latency_ms": 7.6883333333333335, + "end_to_end_latency_ms": 14.125333333333332, + "ops_per_second": 70.78219182342052, + "bytes_per_second": 7248096.442718262, + "peak_memory_mb": 0.0, + "memory_efficiency_ratio": 0.0, + "p50_latency": 12.222625, + "p95_latency": 18.055712200000002, + "p99_latency": 18.57420884, + "timestamp": "2026-01-21 16:22:51", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + }, + { + "test_name": "memory", + "language": "java", + "data_size": 102400, + "concurrency": 1, + "put_latency_ms": 0.0, + "get_latency_ms": 0.0, + "end_to_end_latency_ms": 0.0, + "ops_per_second": 0.0, + "bytes_per_second": 0.0, + "peak_memory_mb": 7.44854736328125, + "memory_efficiency_ratio": 0.013110777878840024, + "p50_latency": 0.0, + "p95_latency": 0.0, + "p99_latency": 0.0, + "timestamp": "2026-01-21 16:22:52", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + }, + { + "test_name": "concurrent", + "language": "java", + "data_size": 102400, + "concurrency": 2, + "put_latency_ms": 0.0, + "get_latency_ms": 0.0, + "end_to_end_latency_ms": 12.0850042, + "ops_per_second": 159.58125877696924, + "bytes_per_second": 1.634112089876165e7, + "peak_memory_mb": 0.0, + "memory_efficiency_ratio": 0.0, + "p50_latency": 11.326729, + "p95_latency": 15.798329050000001, + "p99_latency": 15.81029941, + "timestamp": "2026-01-21 16:22:52", + "java_version": "1.8.0_482", + "cpu_count": 14, + "total_memory_gb": 10.6669921875 + } + ] +}