sourceFileCache =
new ConcurrentHashMap<>();
@@ -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).
*
* Falls back to the package-derived relative path if the file can't be found on disk.
*/
@@ -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 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. */
@@ -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 matcher) {
Path[] result = new Path[1];
try {
@@ -120,7 +142,7 @@ private static Path findFile(Path root, String relativeSuffix) {
new SimpleFileVisitor() {
@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;
}
@@ -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(".")
diff --git a/jmh-fork/jmh-core/src/main/java/io/codspeed/ClassFileSourceName.java b/jmh-fork/jmh-core/src/main/java/io/codspeed/ClassFileSourceName.java
new file mode 100644
index 0000000..7ad1e08
--- /dev/null
+++ b/jmh-fork/jmh-core/src/main/java/io/codspeed/ClassFileSourceName.java
@@ -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);
+ }
+}
diff --git a/jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java b/jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java
index c0e0c35..33b6d1e 100644
--- a/jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java
+++ b/jmh-fork/jmh-core/src/test/java/io/codspeed/BenchmarkUriTest.java
@@ -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;
@@ -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,
@@ -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);
+ }
}
diff --git a/jmh-fork/jmh-generator-asm/build.gradle.kts b/jmh-fork/jmh-generator-asm/build.gradle.kts
index 0aace06..43f2905 100644
--- a/jmh-fork/jmh-generator-asm/build.gradle.kts
+++ b/jmh-fork/jmh-generator-asm/build.gradle.kts
@@ -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")
}
diff --git a/jmh-fork/pom.xml b/jmh-fork/pom.xml
index 0ab7e98..5d4ad33 100644
--- a/jmh-fork/pom.xml
+++ b/jmh-fork/pom.xml
@@ -286,7 +286,7 @@ questions.
org.ow2.asm
asm
- 9.0
+ 9.9.1