Skip to content

Commit

Permalink
Exclude virtual threads from inferred spans feature (#3244)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: SylvainJuge <[email protected]>
  • Loading branch information
JonasKunz and SylvainJuge authored Aug 10, 2023
1 parent 0f29fac commit 796c694
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 11 deletions.
4 changes: 4 additions & 0 deletions 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
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}.
*/
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

0 comments on commit 796c694

Please sign in to comment.