diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 49174139b6..712727963a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -31,6 +31,10 @@ Use subheadings with the "=====" level for adding notes for unreleased changes: === Unreleased +[float] +===== Features +* Virtual thread support - {pull}3244[#3244] + [float] ===== Bug fixes * Fix JVM memory usage capture - {pull}3279[#3279] @@ -1705,4 +1709,4 @@ Transactions are named based on your resources (`ResourceClass#resourceMethod`). you will still want to set the `service_name` explicitly. But it helps getting started and seeing data easier, as there are no required configuration options anymore. - In the future we will most likely determine more useful application names for Servlet API-based applications. \ No newline at end of file + In the future we will most likely determine more useful application names for Servlet API-based applications. diff --git a/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/ThreadUtil.java b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/ThreadUtil.java new file mode 100644 index 0000000000..59c2c6cd07 --- /dev/null +++ b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/ThreadUtil.java @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 + * + * http://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. + */ +package co.elastic.apm.agent.sdk.internal; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.matcher.ElementMatchers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ThreadUtil { + + interface VirtualChecker { + boolean isVirtual(Thread thread); + } + + private static final VirtualChecker VIRTUAL_CHECKER = generateVirtualChecker(); + + + public static boolean isVirtual(Thread thread) { + return VIRTUAL_CHECKER.isVirtual(thread); + } + + /** + * Generates a VirtualChecker based on the current JVM. + * If the JVM does not support virtual threads, a VirtualChecker which always returns false is returned. + *
+ * Otherwise we runtime generate an implementation which invokes Thread.isVirtual(). + * We use runtime proxy generation because Thread.isVirtual() has been added in Java 19 as preview and Java 21 as non preview. + * Therefore we would require a compilation with Java 19 (non-LTS), because Java 20+ does not allow targeting Java 7. + *
+ * Alternatively we could simply invoke Thread.isVirtual via reflection.
+ * However, because this check can be used very frequently we want to avoid the penalty / missing inline capability of reflection.
+ *
+ * @return the implementation for {@link VirtualChecker}.
+ */
+ private static VirtualChecker generateVirtualChecker() {
+ Method isVirtual = null;
+ try {
+ isVirtual = Thread.class.getMethod("isVirtual");
+ isVirtual.invoke(Thread.currentThread()); //invoke to ensure it does not throw exceptions for preview versions
+ Class extends VirtualChecker> impl = new ByteBuddy()
+ .subclass(VirtualChecker.class)
+ .method(ElementMatchers.named("isVirtual"))
+ .intercept(MethodCall.invoke(isVirtual).onArgument(0))
+ .make()
+ .load(VirtualChecker.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
+ .getLoaded();
+ return impl.getConstructor().newInstance();
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ return new VirtualChecker() {
+ @Override
+ public boolean isVirtual(Thread thread) {
+ return false; //virtual threads are not supported, therefore no thread is virtual
+ }
+ };
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/internal/ThreadUtilTest.java b/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/internal/ThreadUtilTest.java
new file mode 100644
index 0000000000..d14d037b10
--- /dev/null
+++ b/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/internal/ThreadUtilTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you 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
+ *
+ * http://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.
+ */
+package co.elastic.apm.agent.sdk.internal;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+public class ThreadUtilTest {
+
+ @Test
+ public void checkPlatformThreadVirtual() {
+ Thread t1 = new Thread();
+ assertThat(ThreadUtil.isVirtual(t1)).isFalse();
+ }
+
+ @Test
+ @DisabledForJreRange(max = JRE.JAVA_20)
+ public void checkVirtualThreadVirtual() throws Exception {
+ Runnable task = () -> {
+ };
+ Thread thread = (Thread) Thread.class.getMethod("startVirtualThread", Runnable.class).invoke(null, task);
+ assertThat(ThreadUtil.isVirtual(thread)).isTrue();
+ }
+}
diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingActivationListener.java b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingActivationListener.java
index 277759536f..2e299f63d5 100644
--- a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingActivationListener.java
+++ b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingActivationListener.java
@@ -21,6 +21,7 @@
import co.elastic.apm.agent.impl.ActivationListener;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.transaction.AbstractSpan;
+import co.elastic.apm.agent.sdk.internal.ThreadUtil;
import java.util.Objects;
@@ -40,7 +41,7 @@ public ProfilingActivationListener(ElasticApmTracer tracer) {
@Override
public void beforeActivate(AbstractSpan> context) {
- if (context.isSampled()) {
+ if (context.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) {
AbstractSpan> active = tracer.getActive();
profiler.onActivation(context.getTraceContext(), active != null ? active.getTraceContext() : null);
}
@@ -48,7 +49,7 @@ public void beforeActivate(AbstractSpan> context) {
@Override
public void afterDeactivate(AbstractSpan> deactivatedContext) {
- if (deactivatedContext.isSampled()) {
+ if (deactivatedContext.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) {
AbstractSpan> active = tracer.getActive();
profiler.onDeactivation(deactivatedContext.getTraceContext(), active != null ? active.getTraceContext() : null);
}
diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingConfiguration.java b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingConfiguration.java
index 8bad530dde..a58b4e67ea 100644
--- a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingConfiguration.java
+++ b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/ProfilingConfiguration.java
@@ -18,10 +18,10 @@
*/
package co.elastic.apm.agent.profiler;
+import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.tracer.configuration.ListValueConverter;
import co.elastic.apm.agent.tracer.configuration.TimeDuration;
import co.elastic.apm.agent.tracer.configuration.TimeDurationValueConverter;
-import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.tracer.configuration.WildcardMatcherValueConverter;
import org.stagemonitor.configuration.ConfigurationOption;
import org.stagemonitor.configuration.ConfigurationOptionProvider;
@@ -49,6 +49,8 @@ public class ProfilingConfiguration extends ConfigurationOptionProvider {
"The inferred spans are created after a profiling session has ended.\n" +
"This means there is a delay between the regular and the inferred spans being visible in the UI.\n" +
"\n" +
+ "Only platform threads are supported. Virtual threads are not supported and will not be profiled.\n" +
+ "\n" +
"NOTE: This feature is not available on Windows and on OpenJ9")
.dynamic(true)
.tags("added[1.15.0]", "experimental")
diff --git a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/SamplingProfiler.java b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/SamplingProfiler.java
index 8b1f8daafb..1a33248b5a 100644
--- a/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/SamplingProfiler.java
+++ b/apm-agent-plugins/apm-profiling-plugin/src/main/java/co/elastic/apm/agent/profiler/SamplingProfiler.java
@@ -18,18 +18,20 @@
*/
package co.elastic.apm.agent.profiler;
-import co.elastic.apm.agent.sdk.internal.util.ExecutorUtils;
-import co.elastic.apm.agent.tracer.configuration.CoreConfiguration;
-import co.elastic.apm.agent.tracer.configuration.TimeDuration;
+import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.context.AbstractLifecycleListener;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.transaction.Span;
import co.elastic.apm.agent.impl.transaction.StackFrame;
import co.elastic.apm.agent.impl.transaction.TraceContext;
-import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler;
import co.elastic.apm.agent.profiler.asyncprofiler.JfrParser;
import co.elastic.apm.agent.profiler.collections.Long2ObjectHashMap;
+import co.elastic.apm.agent.sdk.internal.util.ExecutorUtils;
+import co.elastic.apm.agent.sdk.logging.Logger;
+import co.elastic.apm.agent.sdk.logging.LoggerFactory;
+import co.elastic.apm.agent.tracer.configuration.CoreConfiguration;
+import co.elastic.apm.agent.tracer.configuration.TimeDuration;
import co.elastic.apm.agent.tracer.pooling.Allocator;
import co.elastic.apm.agent.tracer.pooling.ObjectPool;
import com.lmax.disruptor.EventFactory;
@@ -39,8 +41,6 @@
import com.lmax.disruptor.Sequence;
import com.lmax.disruptor.SequenceBarrier;
import com.lmax.disruptor.WaitStrategy;
-import co.elastic.apm.agent.sdk.logging.Logger;
-import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import javax.annotation.Nullable;
import java.io.File;
@@ -226,6 +226,17 @@ public CallTree.Root createInstance() {
this.activationEventsFile = activationEventsFile;
}
+ /**
+ * For testing only!
+ * This method must only be called in tests and some period after activation / deactivation events, as otherwise it is racy.
+ *
+ * @param thread the Thread to check.
+ * @return true, if profiling is active for the given thread.
+ */
+ boolean isProfilingActiveOnThread(Thread thread) {
+ return profiledThreads.containsKey(thread.getId());
+ }
+
private synchronized void createFilesIfRequired() throws IOException {
if (jfrFile == null || !jfrFile.exists()) {
jfrFile = File.createTempFile("apm-traces-", ".jfr");
diff --git a/apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/SamplingProfilerTest.java b/apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/SamplingProfilerTest.java
index 6204d4d03e..6e5816777e 100644
--- a/apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/SamplingProfilerTest.java
+++ b/apm-agent-plugins/apm-profiling-plugin/src/test/java/co/elastic/apm/agent/profiler/SamplingProfilerTest.java
@@ -20,29 +20,33 @@
import co.elastic.apm.agent.MockReporter;
import co.elastic.apm.agent.MockTracer;
+import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.configuration.SpyConfiguration;
-import co.elastic.apm.agent.tracer.configuration.TimeDuration;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.transaction.Span;
import co.elastic.apm.agent.impl.transaction.Transaction;
-import co.elastic.apm.agent.common.util.WildcardMatcher;
import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon;
import co.elastic.apm.agent.tracer.Scope;
+import co.elastic.apm.agent.tracer.configuration.TimeDuration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
import org.stagemonitor.configuration.ConfigurationRegistry;
import javax.annotation.Nullable;
import java.io.IOException;
+import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -164,6 +168,7 @@ void testProfileTransaction() throws Exception {
// makes sure that the rest will be captured by another profiling session
// this tests that restoring which threads to profile works
Thread.sleep(600);
+ assertThat(profiler.isProfilingActiveOnThread(Thread.currentThread())).isTrue();
aInferred(transaction);
} finally {
transaction.end();
@@ -195,6 +200,38 @@ void testProfileTransaction() throws Exception {
assertThat(inferredSpanD.get().isChildOf(inferredSpanC.get())).isTrue();
}
+ @Test
+ @DisabledForJreRange(max = JRE.JAVA_20)
+ void testVirtualThreadsExcluded() throws Exception {
+ setupProfiler(true);
+ awaitProfilerStarted(profiler);
+
+ AtomicReference