Skip to content
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
1 change: 1 addition & 0 deletions jmh-fork/jmh-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies {
api("net.sf.jopt-simple:jopt-simple:5.0.4")
api("org.apache.commons:commons-math3:3.6.1")
implementation("com.google.code.gson:gson:2.11.0")
implementation("org.ow2.asm:asm:9.9.1")
testImplementation("junit:junit:4.13.2")
}

Expand Down
4 changes: 4 additions & 0 deletions jmh-fork/jmh-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ questions.
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</dependency>
</dependencies>

<properties>
Expand Down
65 changes: 45 additions & 20 deletions jmh-fork/jmh-core/src/main/java/io/codspeed/BenchmarkUri.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.openjdk.jmh.infra.BenchmarkParams;

/** Builds CodSpeed benchmark URIs in the format: {file_path}::{classQName}::{method}[{params}] */
public class BenchmarkUri {

static final String[] SOURCE_EXTENSIONS = {"java", "kt", "scala", "groovy"};

private static volatile Path cachedGitRoot;
private static final ConcurrentHashMap<String, String> sourceFileCache =
new ConcurrentHashMap<>();
Expand Down Expand Up @@ -61,7 +68,8 @@ static String buildBenchName(String method, BenchmarkParams params) {

/**
* Resolves the source file path relative to the git root for a given fully qualified class name.
* Searches from the git root for a .java file matching the class's package structure.
* Uses the class file's {@code SourceFile} attribute first, then searches from the git root for a
* matching source file across known extensions (java, kt, scala, groovy).
*
* <p>Falls back to the package-derived relative path if the file can't be found on disk.
*/
Expand All @@ -70,23 +78,38 @@ static String resolveSourceFile(String classQName) {
}

private static String resolveSourceFileUncached(String classQName) {
// Handle inner classes: com.example.Outer$Inner -> com.example.Outer
String outerClass = classQName;
int dollarIdx = outerClass.indexOf('$');
if (dollarIdx != -1) {
outerClass = outerClass.substring(0, dollarIdx);
}

String relativePath = outerClass.replace('.', '/') + ".java";
Path gitRoot = findGitRoot();

Path found = findFile(gitRoot, relativePath);
if (found != null) {
// Normalize to forward slashes for consistent URIs across platforms
return gitRoot.relativize(found).toString().replace('\\', '/');
}
return resolveSourceFile(findGitRoot(), classQName, ClassFileSourceName.read(classQName));
}

return relativePath;
static String resolveSourceFile(Path root, String classQName, String sourceFileName) {
// Handle inner classes: com.example.Outer$Inner -> com.example.Outer
int dollarIdx = classQName.indexOf('$');
String outerClass = dollarIdx == -1 ? classQName : classQName.substring(0, dollarIdx);
Path classPath = Paths.get(outerClass.replace('.', '/'));
Path pkgDir = classPath.getParent();
String simpleName = classPath.getFileName().toString();

// The SourceFile attribute is authoritative when present; the extension search only covers
// classes compiled without it.
List<String> names =
sourceFileName != null
? Collections.singletonList(sourceFileName)
: Arrays.stream(SOURCE_EXTENSIONS)
.map(ext -> simpleName + "." + ext)
.collect(Collectors.toList());

Path found =
findFile(
root,
file ->
(pkgDir == null || file.getParent().endsWith(pkgDir))
&& names.contains(file.getFileName().toString()));

Path result =
found != null
? root.relativize(found)
: pkgDir == null ? Paths.get(names.get(0)) : pkgDir.resolve(names.get(0));
return result.toString().replace('\\', '/');
}

/** Walks up from the CWD to find the nearest .git directory, returns its parent. */
Expand All @@ -110,8 +133,7 @@ static Path findGitRoot() {
return fallback;
}

private static Path findFile(Path root, String relativeSuffix) {
String suffix = "/" + relativeSuffix;
private static Path findFile(Path root, Predicate<Path> matcher) {
Path[] result = new Path[1];

try {
Expand All @@ -120,7 +142,7 @@ private static Path findFile(Path root, String relativeSuffix) {
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(suffix) || file.equals(root.resolve(relativeSuffix))) {
if (matcher.test(file)) {
result[0] = file;
return FileVisitResult.TERMINATE;
}
Expand All @@ -129,6 +151,9 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.equals(root)) {
return FileVisitResult.CONTINUE;
}
String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "";
// Skip hidden dirs, build outputs, and VCS dirs
if (dirName.startsWith(".")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.codspeed;

import java.io.IOException;
import java.io.InputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

/**
* Reads the {@code SourceFile} attribute of a compiled class, which names the file the class was
* compiled from ({@code MyBench.kt}, {@code Foo.scala}, {@code benchmarks.kt}, ...).
*/
final class ClassFileSourceName {

private ClassFileSourceName() {}

/**
* Reads the source file name declared by the given class.
*
* @param classQName fully qualified class name
* @return the declared source file name, or null if the class isn't on the classpath, can't be
* parsed, or carries no {@code SourceFile} attribute
*/
static String read(String classQName) {
InputStream stream = openClassFile(classQName.replace('.', '/') + ".class");
if (stream == null) {
return null;
}

try (InputStream in = stream) {
String[] source = {null};
new ClassReader(in)
.accept(
new ClassVisitor(Opcodes.ASM9) {
@Override
public void visitSource(String name, String debug) {
source[0] = name;
}
},
ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
return source[0];
} catch (IOException | RuntimeException e) {
return null;
}
}

private static InputStream openClassFile(String resource) {
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
if (contextLoader != null) {
InputStream stream = contextLoader.getResourceAsStream(resource);
if (stream != null) {
return stream;
}
}

ClassLoader ownLoader = ClassFileSourceName.class.getClassLoader();
return ownLoader == null ? null : ownLoader.getResourceAsStream(resource);
}
}
88 changes: 88 additions & 0 deletions jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.codspeed;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.infra.BenchmarkParams;
import org.openjdk.jmh.infra.IterationParams;
Expand All @@ -13,6 +19,15 @@

public class BenchmarkUriTest {

@Rule public TemporaryFolder tempFolder = new TemporaryFolder();

private static Path createSource(Path root, String relativePath) throws IOException {
Path file = root.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.createFile(file);
return file;
}

private static BenchmarkParams makeParams(String benchmark, WorkloadParams workloadParams) {
return new BenchmarkParams(
benchmark,
Expand Down Expand Up @@ -97,4 +112,77 @@ public void testFullUriWithParams() {
assertEquals(
"com/nonexistent/MyBenchmark.java::com.nonexistent.MyBenchmark::encode[65536]", uri);
}

@Test
public void testResolveSourceFileKotlinByExtension() throws IOException {
Path root = tempFolder.getRoot().toPath();
createSource(root, "src/jmh/kotlin/com/example/KotlinBench.kt");
String path = BenchmarkUri.resolveSourceFile(root, "com.example.KotlinBench", null);
assertEquals("src/jmh/kotlin/com/example/KotlinBench.kt", path);
}

@Test
public void testResolveSourceFileScalaByExtension() throws IOException {
Path root = tempFolder.getRoot().toPath();
createSource(root, "src/jmh/scala/com/example/ScalaBench.scala");
String path = BenchmarkUri.resolveSourceFile(root, "com.example.ScalaBench", null);
assertEquals("src/jmh/scala/com/example/ScalaBench.scala", path);
}

@Test
public void testResolveSourceFileKotlinFileNameDiffersFromClass() throws IOException {
Path root = tempFolder.getRoot().toPath();
createSource(root, "src/main/kotlin/com/example/benchmarks.kt");
String path = BenchmarkUri.resolveSourceFile(root, "com.example.KotlinBench", "benchmarks.kt");
assertEquals("src/main/kotlin/com/example/benchmarks.kt", path);
}

@Test
public void testResolveSourceFileFallbackWithSourceFileName() {
Path root = tempFolder.getRoot().toPath();
String path =
BenchmarkUri.resolveSourceFile(root, "com.example.KotlinBench", "KotlinBench.kt");
assertEquals("com/example/KotlinBench.kt", path);
}

@Test
public void testResolveSourceFileFallbackWithoutSourceFileName() {
Path root = tempFolder.getRoot().toPath();
String path = BenchmarkUri.resolveSourceFile(root, "com.example.KotlinBench", null);
assertEquals("com/example/KotlinBench.java", path);
}

@Test
public void testResolveSourceFileSkipsBuildDirectory() throws IOException {
Path root = tempFolder.getRoot().toPath();
createSource(root, "build/com/example/Skipped.kt");
String path = BenchmarkUri.resolveSourceFile(root, "com.example.Skipped", null);
assertEquals("com/example/Skipped.java", path);
}

@Test
public void testResolveSourceFileInnerClassWithKotlinSource() throws IOException {
Path root = tempFolder.getRoot().toPath();
createSource(root, "com/example/Outer.kt");
String path = BenchmarkUri.resolveSourceFile(root, "com.example.Outer$Inner", null);
assertEquals("com/example/Outer.kt", path);
}

@Test
public void testClassFileSourceNameReadsOwnSourceFile() {
String sourceFile = ClassFileSourceName.read("io.codspeed.BenchmarkUriTest");
assertEquals("BenchmarkUriTest.java", sourceFile);
}

@Test
public void testClassFileSourceNameReturnsNullForMissingClass() {
String sourceFile = ClassFileSourceName.read("com.nonexistent.Missing");
assertNull(sourceFile);
}

@Test
public void testResolveSourceFileEndToEndForThisTestClass() {
String path = BenchmarkUri.resolveSourceFile("io.codspeed.BenchmarkUriTest");
assertEquals("jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java", path);
}
}
2 changes: 1 addition & 1 deletion jmh-fork/jmh-generator-asm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
dependencies {
api(project(":jmh-core"))
api(project(":jmh-generator-reflection"))
api("org.ow2.asm:asm:9.0")
api("org.ow2.asm:asm:9.9.1")
}
2 changes: 1 addition & 1 deletion jmh-fork/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ questions.
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.0</version>
<version>9.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
Loading