Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/benchmark-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Benchmark PR

on:
pull_request:
types: [opened, synchronize, reopened, labeled]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

env:
BENCHMARK_MODULE: exporters/otlp/common
BENCHMARK_CLASSES: StringMarshalBenchmark

jobs:
sdk-benchmark:
name: Benchmark SDK (Java ${{ matrix.test-java-version }})
if: contains(github.event.pull_request.labels.*.name, 'run benchmarks')
strategy:
fail-fast: false
matrix:
test-java-version:
- 17
- 24
runs-on: oracle-bare-metal-64cpu-512gb-x86-64
container:
image: ubuntu:24.04@sha256:353675e2a41babd526e2b837d7ec780c2a05bca0164f7ea5dbbd433d21d166fc
timeout-minutes: 20 # since there is only a single bare metal runner across all repos
steps:
- name: Install Git
run: |
apt-get update
apt-get install -y git

- name: Configure Git safe directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- id: setup-java-test
name: Set up Java ${{ matrix.test-java-version }} for tests
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: temurin
java-version: ${{ matrix.test-java-version }}

- id: setup-java
name: Set up Java for build
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: temurin
java-version: 17

- name: Set up gradle
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3

- name: Build Benchmark
run: ./gradlew jmhJar

- name: Run Benchmark
run: >
${{ steps.setup-java-test.outputs.path }}/bin/java
-jar ${{ env.BENCHMARK_MODULE }}/build/libs/opentelemetry-*-jmh.jar
-jvmArgs="--add-opens=java.base/java.lang=ALL-UNNAMED"
-rf json
${{ env.BENCHMARK_CLASSES }}

- name: Rename results
run: mv jmh-result.json jmh-result-pr.json

- name: Switch to main branch
run: git checkout origin/main

- name: Build Benchmark on main branch
run: ./gradlew jmhJar

- name: Run Benchmark on main branch
run: >
${{ steps.setup-java-test.outputs.path }}/bin/java
-jar ${{ env.BENCHMARK_MODULE }}/build/libs/opentelemetry-*-jmh.jar
-rf json
${{ env.BENCHMARK_CLASSES }}

- name: Rename results
run: mv jmh-result.json jmh-result-main.json

- name: Upload benchmark results
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmark-results-java-${{ matrix.test-java-version }}
path: |
jmh-result-pr.json
jmh-result-main.json
9 changes: 9 additions & 0 deletions buildSrc/src/main/kotlin/otel.jmh-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ jmh {
if (jmhIncludeSingleClass != null) {
includes.add(jmhIncludeSingleClass as String)
}

val testJavaVersion = gradle.startParameter.projectProperties.get("testJavaVersion")?.let(JavaVersion::toVersion)
if (testJavaVersion != null) {
val javaExecutable = javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(testJavaVersion.majorVersion))
}.get().executablePath.asFile.absolutePath

jvm.set(javaExecutable)
}
}

jmhReport {
Expand Down
49 changes: 49 additions & 0 deletions exporters/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@ plugins {
description = "OpenTelemetry Exporter Common"
otelJava.moduleName.set("io.opentelemetry.exporter.internal")

java {
sourceSets {
create("java9") {
java {
srcDir("src/main/java9")
}
// Make java9 source set depend on main source set
// since VarHandleStringEncoder implements StringEncoder from the main source set
compileClasspath += sourceSets.main.get().output + sourceSets.main.get().compileClasspath
}
}
}

// Configure java9 compilation to see main source classes
sourceSets.named("java9") {
compileClasspath += sourceSets.main.get().output
}

tasks.named<JavaCompile>("compileJava9Java") {
options.release.set(9)
}

tasks.named<Jar>("jar") {
manifest {
attributes["Multi-Release"] = "true"
}
from(sourceSets.named("java9").get().output) {
into("META-INF/versions/9")
}
}

// Configure test to include java9 classes when running on Java 9+
// so that StringEncoderHolder.createUnsafeEncoder() can instantiate the Java 9 version
val javaVersion = JavaVersion.current()
if (javaVersion >= JavaVersion.VERSION_1_9) {
sourceSets.named("test") {
runtimeClasspath += sourceSets.named("java9").get().output
}
}

val versions: Map<String, String> by project
dependencies {
api(project(":api:all"))
Expand Down Expand Up @@ -79,6 +119,15 @@ tasks {
check {
dependsOn(testing.suites)
}

withType<Test> {
// Allow VarHandle access to String internals
// generally users won't do this and so won't get the VarHandle implementation
// but the Java agent is able to automatically open these modules
// (see ModuleOpener.java in that repository)
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions") // needed for Java 8
}
}

afterEvaluate {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.internal.marshal;

import java.io.IOException;

/**
* This class contains shared logic for UTF-8 encoding operations while allowing subclasses to
* implement different mechanisms for accessing String internal byte arrays (e.g., Unsafe vs
* VarHandle).
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
abstract class AbstractStringEncoder implements StringEncoder {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code in this class is just moved (cut-and-paste) from StatelessMarshalerUtil


private final FallbackStringEncoder fallback = new FallbackStringEncoder();

@Override
public final void writeUtf8(CodedOutputStream output, String string, int utf8Length)
throws IOException {
// if the length of the latin1 string and the utf8 output are the same then the string must be
// composed of only 7bit characters and can be directly copied to the output
if (string.length() == utf8Length && isLatin1(string)) {
byte[] bytes = getStringBytes(string);
output.write(bytes, 0, bytes.length);
} else {
fallback.writeUtf8(output, string, utf8Length);
}
}

@Override
public final int getUtf8Size(String string) {
if (isLatin1(string)) {
byte[] bytes = getStringBytes(string);
// latin1 bytes with negative value (most significant bit set) are encoded as 2 bytes in utf8
return string.length() + countNegative(bytes);
}

return fallback.getUtf8Size(string);
}

protected abstract byte[] getStringBytes(String string);

protected abstract boolean isLatin1(String string);

protected abstract long getLong(byte[] bytes, int offset);

// Inner loop can process at most 8 * 255 bytes without overflowing counter. To process more bytes
// inner loop has to be run multiple times.
private static final int MAX_INNER_LOOP_SIZE = 8 * 255;
// mask that selects only the most significant bit in every byte of the long
private static final long MOST_SIGNIFICANT_BIT_MASK = 0x8080808080808080L;

/** Returns the count of bytes with negative value. */
private int countNegative(byte[] bytes) {
int count = 0;
int offset = 0;
// We are processing one long (8 bytes) at a time. In the inner loop we are keeping counts in a
// long where each byte in the long is a separate counter. Due to this the inner loop can
// process a maximum of 8*255 bytes at a time without overflow.
for (int i = 1; i <= bytes.length / MAX_INNER_LOOP_SIZE + 1; i++) {
long tmp = 0; // each byte in this long is a separate counter
int limit = Math.min(i * MAX_INNER_LOOP_SIZE, bytes.length & ~7);
for (; offset < limit; offset += 8) {
long value = getLong(bytes, offset);
// Mask the value keeping only the most significant bit in each byte and then shift this bit
// to the position of the least significant bit in each byte. If the input byte was not
// negative then after this transformation it will be zero, if it was negative then it will
// be one.
tmp += (value & MOST_SIGNIFICANT_BIT_MASK) >>> 7;
}
// sum up counts
if (tmp != 0) {
for (int j = 0; j < 8; j++) {
count += (int) (tmp & 0xff);
tmp = tmp >>> 8;
}
}
}

// Handle remaining bytes. Previous loop processes 8 bytes a time, if the input size is not
// divisible with 8 the remaining bytes are handled here.
for (int i = offset; i < bytes.length; i++) {
// same as if (bytes[i] < 0) count++;
count += bytes[i] >>> 31;
}
return count;
}
}
Loading
Loading