Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude virtual threads from inferred spans feature #3244

Merged
merged 14 commits into from
Aug 10, 2023
Merged
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
6 changes: 5 additions & 1 deletion CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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.
In the future we will most likely determine more useful application names for Servlet API-based applications.
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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}.
*/
Comment on lines +42 to +54
Copy link
Member

Choose a reason for hiding this comment

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

Nice ! I think that's definitely a good approach we could use for other deprecated or soon-to-be-removed APIs like the security manager.

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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -40,15 +41,15 @@ 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);
}
}

@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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Boolean> profilingActive = new AtomicReference<>();
Runnable task = () -> {
Transaction transaction = tracer.startRootTransaction(null).withName("transaction");
try (Scope scope = transaction.activateInScope()) {
// makes sure that the rest will be captured by another profiling session
// this tests that restoring which threads to profile works
try {
Thread.sleep(600);
} catch (Exception e) {
throw new RuntimeException(e);
}
profilingActive.set(profiler.isProfilingActiveOnThread(Thread.currentThread()));
} finally {
transaction.end();
}
};

Method startVirtualThread = Thread.class.getMethod("startVirtualThread", Runnable.class);
Thread virtual = (Thread) startVirtualThread.invoke(null, task);
virtual.join();

assertThat(profilingActive.get()).isFalse();

}


@Test
void testPostProcessingDisabled() throws Exception {
setupProfiler(true);
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2608,6 +2608,8 @@ The <<config-profiling-inferred-spans-sampling-interval, `profiling_inferred_spa
The inferred spans are created after a profiling session has ended.
This means there is a delay between the regular and the inferred spans being visible in the UI.

Only platform threads are supported. Virtual threads are not supported and will not be profiled.

NOTE: This feature is not available on Windows and on OpenJ9

<<configuration-dynamic, image:./images/dynamic-config.svg[] >>
Expand Down Expand Up @@ -4586,6 +4588,8 @@ Example: `5ms`.
# The inferred spans are created after a profiling session has ended.
# This means there is a delay between the regular and the inferred spans being visible in the UI.
#
# Only platform threads are supported. Virtual threads are not supported and will not be profiled.
#
# NOTE: This feature is not available on Windows and on OpenJ9
#
# This setting can be changed at runtime
Expand Down
Loading