Skip to content

Commit

Permalink
Inject coverage store field into Thread class
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-tkachenko-datadog committed Jul 12, 2024
1 parent 8519757 commit 3dce858
Show file tree
Hide file tree
Showing 27 changed files with 482 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
package datadog.trace.civisibility.config;

import datadog.trace.civisibility.ipc.Serializer;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JvmInfo {

private static final Logger LOGGER =
LoggerFactory.getLogger(ModuleExecutionSettingsFactoryImpl.class);

public static final JvmInfo CURRENT_JVM =
new JvmInfo(
System.getProperty("java.runtime.name"),
System.getProperty("java.version"),
System.getProperty("java.vendor"));
System.getProperty("java.class.version"),
System.getProperty("java.vendor"),
System.getProperty("java.home"));

private static final int JAVA_8_CLASS_VERSION = 52;

private final String name;
private final String version;
private final int majorClassVersion;
private final String vendor;
private final Path home;

@SuppressForbidden // split on "\." uses fast path
public JvmInfo(String name, String version, String classVersion, String vendor, String home) {
this.name = name;
this.version = version;
this.vendor = vendor;
this.home = home != null ? Paths.get(home) : null;

int majorClassVersion = -1;
try {
if (classVersion != null) {
String[] classVersionTokens = classVersion.split("\\.");
majorClassVersion = Integer.parseInt(classVersionTokens[0]);
}
} catch (Exception e) {
LOGGER.debug("Could not parse class version {} for JVM {}", classVersion, this);
}
this.majorClassVersion = majorClassVersion;
}

public JvmInfo(String name, String version, String vendor) {
private JvmInfo(String name, String version, int majorClassVersion, String vendor, Path home) {
this.name = name;
this.version = version;
this.vendor = vendor;
this.home = home;
this.majorClassVersion = majorClassVersion;
}

public String getName() {
Expand All @@ -30,10 +65,22 @@ public String getVersion() {
return version;
}

public int getMajorClassVersion() {
return majorClassVersion;
}

public String getVendor() {
return vendor;
}

public Path getHome() {
return home;
}

public boolean isModular() {
return majorClassVersion > JAVA_8_CLASS_VERSION;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -45,12 +92,14 @@ public boolean equals(Object o) {
JvmInfo jvmInfo = (JvmInfo) o;
return Objects.equals(name, jvmInfo.name)
&& Objects.equals(version, jvmInfo.version)
&& Objects.equals(vendor, jvmInfo.vendor);
&& Objects.equals(majorClassVersion, jvmInfo.majorClassVersion)
&& Objects.equals(vendor, jvmInfo.vendor)
&& Objects.equals(home, jvmInfo.home);
}

@Override
public int hashCode() {
return Objects.hash(name, version, vendor);
return Objects.hash(name, version, majorClassVersion, vendor, home);
}

@Override
Expand All @@ -61,21 +110,33 @@ public String toString() {
+ '\''
+ ", version='"
+ version
+ ", majorClassVersion='"
+ majorClassVersion
+ '\''
+ ", vendor='"
+ vendor
+ '\''
+ ", home='"
+ home
+ '\''
+ '}';
}

public static void serialize(Serializer serializer, JvmInfo jvmInfo) {
serializer.write(jvmInfo.name);
serializer.write(jvmInfo.version);
serializer.write(jvmInfo.majorClassVersion);
serializer.write(jvmInfo.vendor);
serializer.write(jvmInfo.home != null ? String.valueOf(jvmInfo.home) : null);
}

public static JvmInfo deserialize(ByteBuffer buf) {
String name = Serializer.readString(buf);
String version = Serializer.readString(buf);
int majorClassVersion = Serializer.readInt(buf);
String vendor = Serializer.readString(buf);
String home = Serializer.readString(buf);
return new JvmInfo(
Serializer.readString(buf), Serializer.readString(buf), Serializer.readString(buf));
name, version, majorClassVersion, vendor, home != null ? Paths.get(home) : null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ private static final class JvmVersionOutputParser
public JvmInfo parse(InputStream inputStream) throws IOException {
String name = null;
String version = null;
String classVersion = null;
String vendor = null;
String home = null;

BufferedReader bis =
new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
Expand All @@ -74,11 +76,15 @@ public JvmInfo parse(InputStream inputStream) throws IOException {
name = getPropertyValue(line);
} else if (line.contains("java.version ")) {
version = getPropertyValue(line);
} else if (line.contains("java.class.version ")) {
classVersion = getPropertyValue(line);
} else if (line.contains("java.vendor ")) {
vendor = getPropertyValue(line);
} else if (line.contains("java.home ")) {
home = getPropertyValue(line);
}
}
return new JvmInfo(name, version, vendor);
return new JvmInfo(name, version, classVersion, vendor, home);
}

private String getPropertyValue(String line) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import datadog.trace.api.config.CiVisibilityConfig;
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.GitInfoProvider;
import datadog.trace.civisibility.coverage.instrumentation.store.JvmPatcher;
import datadog.trace.civisibility.git.tree.GitDataUploader;
import datadog.trace.civisibility.source.index.RepoIndex;
import datadog.trace.civisibility.source.index.RepoIndexProvider;
import datadog.trace.civisibility.utils.FileUtils;
import datadog.trace.util.Strings;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -65,14 +68,6 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
boolean testSkippingEnabled = isTestSkippingEnabled(ciVisibilitySettings);
boolean flakyTestRetriesEnabled = isFlakyTestRetriesEnabled(ciVisibilitySettings);
boolean earlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled(ciVisibilitySettings);
Map<String, String> systemProperties =
getPropertiesPropagatedToChildProcess(
itrEnabled,
codeCoverageEnabled,
testSkippingEnabled,
flakyTestRetriesEnabled,
earlyFlakeDetectionEnabled);

LOGGER.info(
"CI Visibility settings ({}, {}):\n"
+ "Intelligent Test Runner - {},\n"
Expand Down Expand Up @@ -109,6 +104,24 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
Map<String, Collection<TestIdentifier>> knownTestsByModuleName =
earlyFlakeDetectionEnabled ? getKnownTests(tracerEnvironment) : null;

Map<String, String> systemProperties =
getPropertiesPropagatedToChildProcess(
itrEnabled,
codeCoverageEnabled,
testSkippingEnabled,
flakyTestRetriesEnabled,
earlyFlakeDetectionEnabled);

List<String> jvmOptions = new ArrayList<>();
if (codeCoverageEnabled
&& config.isCiVisibilityJvmPatchingEnabled()
// null home or negative class version means we couldn't get JVM details that are necessary
// for patching
&& jvmInfo.getHome() != null
&& jvmInfo.getMajorClassVersion() > 0) {
jvmOptions.addAll(patchJvm(jvmInfo));
}

List<String> coverageEnabledPackages = getCoverageEnabledPackages(codeCoverageEnabled);
return new ModuleExecutionSettings(
itrEnabled,
Expand All @@ -123,13 +136,38 @@ public ModuleExecutionSettings create(JvmInfo jvmInfo, @Nullable String moduleNa
? ciVisibilitySettings.getEarlyFlakeDetectionSettings()
: EarlyFlakeDetectionSettings.DEFAULT,
systemProperties,
jvmOptions,
itrCorrelationId,
skippableTestsByModuleName,
flakyTests,
knownTestsByModuleName,
coverageEnabledPackages);
}

/**
* @return The list of JVM options that are needed to prepend the patch to the bootstrap classpath
*/
private List<String> patchJvm(JvmInfo jvmInfo) {
try {
JvmPatcher patcher = new JvmPatcher(jvmInfo);
Path patchPath = patcher.createPatch();

Thread cleanupPatch =
new Thread(() -> FileUtils.deleteSafely(patchPath), "dd-ci-vis-patched-classes-remove");
Runtime.getRuntime().addShutdownHook(cleanupPatch);

if (jvmInfo.isModular()) {
return Arrays.asList("--patch-module", String.format("java.base=%s", patchPath));
} else {
return Collections.singletonList(String.format("-Xbootclasspath/p:%s", patchPath));
}

} catch (Exception e) {
LOGGER.debug("Failed to patch {}", jvmInfo, e);
return Collections.emptyList();
}
}

private TracerEnvironment buildTracerEnvironment(
String repositoryRoot, JvmInfo jvmInfo, @Nullable String moduleName) {
GitInfo gitInfo = GitInfoProvider.INSTANCE.getGitInfo(repositoryRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static ByteBuffer serialize(ModuleExecutionSettings settings) {
EarlyFlakeDetectionSettingsSerializer.serialize(s, settings.getEarlyFlakeDetectionSettings());

s.write(settings.getSystemProperties());
s.write(settings.getJvmOptions());
s.write(settings.getItrCorrelationId());
s.write(
settings.getSkippableTestsByModule(),
Expand All @@ -56,6 +57,8 @@ public static ModuleExecutionSettings deserialize(ByteBuffer buffer) {
EarlyFlakeDetectionSettingsSerializer.deserialize(buffer);

Map<String, String> systemProperties = Serializer.readStringMap(buffer);
List<String> jvmOptions = Serializer.readStringList(buffer);

String itrCorrelationId = Serializer.readString(buffer);
Map<String, Collection<TestIdentifier>> skippableTestsByModule =
Serializer.readMap(
Expand All @@ -78,6 +81,7 @@ public static ModuleExecutionSettings deserialize(ByteBuffer buffer) {
flakyTestRetriesEnabled,
earlyFlakeDetectionSettings,
systemProperties,
jvmOptions,
itrCorrelationId,
skippableTestsByModule,
flakyTests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package datadog.trace.civisibility.coverage.instrumentation.store;

import datadog.communication.util.IOThrowingFunction;
import datadog.trace.civisibility.config.JvmInfo;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class CoreJvmClassReader {

private static final String JAVA_BASE_MODULE_RELATIVE_PATH = "jmods/java.base.jmod";

// java.home contains the "/jre" segment for Java 8 distributions
private static final String RT_JAR_RELATIVE_PATH = "lib/rt.jar";

/**
* Performs an action on the bytecode of a core JVM class
*
* @param jvm The JVM distribution
* @param className The name of the class (as returned by {@link Class#getName}, e.g. {@code
* java.lang.Thread})
* @param action The action to be performed on the class bytecode stream
* @return The return value of the action
* @param <T> The type of the return value
* @throws IOException If the stream could not be retrieved
*/
public <T> T withClassStream(
JvmInfo jvm, String className, IOThrowingFunction<InputStream, T> action) throws IOException {
if (jvm.isModular()) {
return withClassStreamModularJdk(jvm.getHome(), className, action);
} else {
return withClassStreamPreModularJdk(jvm.getHome(), className, action);
}
}

private <T> T withClassStreamModularJdk(
Path jvmHome, String className, IOThrowingFunction<InputStream, T> action)
throws IOException {
Path javaBaseModule = jvmHome.resolve(JAVA_BASE_MODULE_RELATIVE_PATH);
return withZipEntry(
javaBaseModule, "classes/" + className.replace('.', '/') + ".class", action);
}

private <T> T withClassStreamPreModularJdk(
Path jvmHome, String className, IOThrowingFunction<InputStream, T> action)
throws IOException {
Path rtJar = jvmHome.resolve(RT_JAR_RELATIVE_PATH);
return withZipEntry(rtJar, className.replace('.', '/') + ".class", action);
}

private static <T> T withZipEntry(
Path zipFilePath, String entryName, IOThrowingFunction<InputStream, T> action)
throws IOException {
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
ZipEntry entry = zipFile.getEntry(entryName);
if (entry == null) {
throw new IOException("Entry " + entryName + " not found in zip file " + zipFilePath);
}
try (InputStream entryStream = zipFile.getInputStream(entry)) {
return action.apply(entryStream);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package datadog.trace.civisibility.coverage.instrumentation.store;

import static datadog.trace.api.civisibility.coverage.CoverageBridge.COVERAGE_PROBES_FIELD_NAME;

import datadog.trace.api.civisibility.coverage.CoverageBridge;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

/**
* A {@link ClassVisitor} that injects an instance field named {@link
* CoverageBridge#COVERAGE_PROBES_FIELD_NAME} of type {@link Object} into the visited class
*/
public class CoverageStoreFieldInjector extends ClassVisitor {

public CoverageStoreFieldInjector(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}

@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PUBLIC, COVERAGE_PROBES_FIELD_NAME, "Ljava/lang/Object;", null, null)
.visitEnd();
super.visitEnd();
}
}
Loading

0 comments on commit 3dce858

Please sign in to comment.