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
39 changes: 38 additions & 1 deletion instrumentation/runtime-telemetry/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ sourceSets {
setSrcDirs(listOf("src/testJava17/java"))
}
}
create("testJava21") {
java {
setSrcDirs(listOf("src/testJava21/java"))
}
}
}

for (version in mrJarVersions) {
Expand Down Expand Up @@ -60,12 +65,23 @@ configurations {
named("testJava17RuntimeOnly") {
extendsFrom(configurations["testRuntimeOnly"])
}
named("testJava21Implementation") {
extendsFrom(configurations["testImplementation"])
}
named("testJava21RuntimeOnly") {
extendsFrom(configurations["testRuntimeOnly"])
}
}

dependencies {
add("testJava17Implementation", sourceSets.test.get().output)
add("testJava17Implementation", sourceSets["java17"].output)
add("testJava17Implementation", sourceSets.main.get().output)

add("testJava21Implementation", sourceSets.test.get().output)
add("testJava21Implementation", sourceSets["java17"].output)
add("testJava21Implementation", sourceSets["testJava17"].output)
add("testJava21Implementation", sourceSets.main.get().output)
}

tasks {
Expand All @@ -81,6 +97,14 @@ tasks {
options.release.set(17)
}

// Configure testJava21 compilation for Java 21
named<JavaCompile>("compileTestJava21Java") {
dependsOn("compileJava17Java", "compileTestJava17Java")
sourceCompatibility = "21"
targetCompatibility = "21"
options.release.set(21)
}

withType(Jar::class) {
val sourcePathProvider = if (name == "jar") {
{ ss: SourceSet? -> ss?.output }
Expand Down Expand Up @@ -154,6 +178,14 @@ tasks {
// Java 8 tests only
}

// Run Java 21+ tests (virtual threads)
val testJava21 = register<Test>("testJava21") {
dependsOn("compileTestJava21Java")
testClassesDirs = sourceSets["testJava21"].output.classesDirs
classpath = sourceSets["testJava21"].runtimeClasspath
systemProperty("metadataConfig", "Java21")
}

val testJavaVersion = otelProps.testJavaVersion ?: JavaVersion.current()
if (!testJavaVersion.isCompatibleWith(JavaVersion.VERSION_17)) {
named("testG1", Test::class).configure {
Expand All @@ -169,8 +201,13 @@ tasks {
enabled = false
}
}
if (!testJavaVersion.isCompatibleWith(JavaVersion.VERSION_21)) {
named("testJava21", Test::class).configure {
enabled = false
}
}

check {
dependsOn(testJava17, testG1, testPS, testSerial)
dependsOn(testJava17, testJava21, testG1, testPS, testSerial)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import io.opentelemetry.instrumentation.runtimetelemetry.internal.network.NetworkReadHandler;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.network.NetworkWriteHandler;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.threads.ThreadCountHandler;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.threads.VirtualThreadPinnedHandler;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.threads.VirtualThreadSubmitFailedHandler;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
Expand Down Expand Up @@ -72,6 +74,8 @@ static List<RecordedEventHandler> getHandlers(
new ContainerConfigurationHandler(meter, useLegacyCpuCountMetric),
new LongLockHandler(meter),
new ThreadCountHandler(meter),
new VirtualThreadPinnedHandler(meter),
new VirtualThreadSubmitFailedHandler(meter),
new ClassesLoadedHandler(meter),
new MetaspaceSummaryHandler(meter),
new CodeCacheConfigurationHandler(meter),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum JfrFeature {
MEMORY_POOL_METRICS(/* overlapsWithJmx= */ true, /* experimental= */ false),
NETWORK_IO_METRICS(/* overlapsWithJmx= */ false, /* experimental= */ true),
THREAD_METRICS(/* overlapsWithJmx= */ true, /* experimental= */ false),
VIRTUAL_THREAD_METRICS(/* overlapsWithJmx= */ false, /* experimental= */ true),
;

private final boolean overlapsWithJmx;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.runtimetelemetry.internal.threads;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.Constants;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.DurationUtil;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.JfrFeature;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.RecordedEventHandler;
import java.time.Duration;
import java.util.Optional;
import jdk.jfr.consumer.RecordedEvent;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class VirtualThreadPinnedHandler implements RecordedEventHandler {
private static final String METRIC_NAME = "jvm.thread.virtual.pinned";
private static final String METRIC_DESCRIPTION = "Duration of virtual thread pinning";
private static final String EVENT_NAME = "jdk.VirtualThreadPinned";

private final DoubleHistogram histogram;
private final Attributes attributes;

public VirtualThreadPinnedHandler(Meter meter) {
histogram =
meter
.histogramBuilder(METRIC_NAME)
.setDescription(METRIC_DESCRIPTION)
.setUnit(Constants.SECONDS)
.build();

attributes = Attributes.empty();
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public JfrFeature getFeature() {
return JfrFeature.VIRTUAL_THREAD_METRICS;
}

@Override
public void accept(RecordedEvent recordedEvent) {
histogram.record(DurationUtil.toSeconds(recordedEvent.getDuration()), attributes);
}

@Override
public Optional<Duration> getThreshold() {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.runtimetelemetry.internal.threads;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.JfrFeature;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.RecordedEventHandler;
import java.time.Duration;
import java.util.Optional;
import jdk.jfr.consumer.RecordedEvent;

/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class VirtualThreadSubmitFailedHandler implements RecordedEventHandler {
private static final String METRIC_NAME = "jvm.thread.virtual.submit_failed";
private static final String METRIC_DESCRIPTION =
"Number of times a virtual thread failed to be submitted to its scheduler";
private static final String EVENT_NAME = "jdk.VirtualThreadSubmitFailed";

private final LongCounter counter;
private final Attributes attributes;

public VirtualThreadSubmitFailedHandler(Meter meter) {
counter = meter.counterBuilder(METRIC_NAME).setDescription(METRIC_DESCRIPTION).build();

attributes = Attributes.empty();
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public JfrFeature getFeature() {
return JfrFeature.VIRTUAL_THREAD_METRICS;
}

@Override
public void accept(RecordedEvent recordedEvent) {
counter.add(1, attributes);
}

@Override
public Optional<Duration> getThreshold() {
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.runtimetelemetry;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.instrumentation.runtimetelemetry.internal.Constants;
import io.opentelemetry.instrumentation.runtimetelemetry.internal.JfrFeature;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

class JfrVirtualThreadPinnedTest {

@RegisterExtension
JfrExtension jfrExtension =
new JfrExtension(
jfrConfig -> {
jfrConfig.disableAllFeatures();
jfrConfig.enableFeature(JfrFeature.VIRTUAL_THREAD_METRICS);
});

@Test
void shouldHaveVirtualThreadPinnedEvents() throws InterruptedException {
// Thread.ofVirtual() is GA in Java 21; synchronized pinning was removed in Java 24 (JEP 491)
int feature = Runtime.version().feature();
Assumptions.assumeTrue(feature >= 21 && feature < 24, "Requires Java 21-23");
Comment on lines +32 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could consider @EnabledOnJre(JRE.JAVA_21)


CountDownLatch latch = new CountDownLatch(1);

// Run a synchronized block inside a virtual thread to trigger pinning
Object lock = new Object();
Thread vt =
Thread.ofVirtual()
.start(
() -> {
synchronized (lock) {
// Parking inside a synchronized block pins the virtual thread
LockSupport.parkNanos(100_000_000L); // 100ms
}
latch.countDown();
});

assertThat(latch.await(10, SECONDS)).isTrue();
vt.join(5000);

jfrExtension.waitAndAssertMetrics(
metric ->
metric
.hasName("jvm.thread.virtual.pinned")
.hasUnit(Constants.SECONDS)
.hasHistogramSatisfying(histogram -> histogram.hasPointsSatisfying(point -> {})));
}
}
Loading