Skip to content
Draft
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
73 changes: 63 additions & 10 deletions jmh-fork/jmh-core/src/main/java/io/codspeed/BenchmarkUri.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
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 +64,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 +74,70 @@ static String resolveSourceFile(String classQName) {
}

private static String resolveSourceFileUncached(String classQName) {
return resolveSourceFile(findGitRoot(), classQName, ClassFileSourceName.read(classQName));
}

static String resolveSourceFile(Path root, String classQName, String sourceFileName) {
// 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();
int lastDot = outerClass.lastIndexOf('.');
String pkgPath = lastDot != -1 ? outerClass.substring(0, lastDot).replace('.', '/') : "";
String simpleName = lastDot != -1 ? outerClass.substring(lastDot + 1) : outerClass;

if (sourceFileName != null) {
String relativeSuffix = pkgPath.isEmpty() ? sourceFileName : pkgPath + "/" + sourceFileName;
String suffix = "/" + relativeSuffix;
Path exact = root.resolve(relativeSuffix);
Path found =
findFile(
root,
file -> file.toString().replace('\\', '/').endsWith(suffix) || file.equals(exact));
if (found != null) {
return root.relativize(found).toString().replace('\\', '/');
}
}

Path found = findFile(gitRoot, relativePath);
Path found =
findFile(
root,
file -> {
if (!parentEndsWith(file.getParent(), pkgPath)) {
return false;
}
Path fileName = file.getFileName();
if (fileName == null) {
return false;
}
String name = fileName.toString();
for (String ext : SOURCE_EXTENSIONS) {
if (name.equals(simpleName + "." + ext)) {
return true;
}
}
return false;
});
if (found != null) {
// Normalize to forward slashes for consistent URIs across platforms
return gitRoot.relativize(found).toString().replace('\\', '/');
return root.relativize(found).toString().replace('\\', '/');
}

return relativePath;
String fallbackName = sourceFileName != null ? sourceFileName : simpleName + ".java";
return pkgPath.isEmpty() ? fallbackName : pkgPath + "/" + fallbackName;
}

private static boolean parentEndsWith(Path parent, String pkgPath) {
if (pkgPath.isEmpty()) {
return true;
}
if (parent == null) {
return false;
}
String parentStr = parent.toString().replace('\\', '/');
return parentStr.endsWith("/" + pkgPath) || parentStr.equals(pkgPath);
}

/** Walks up from the CWD to find the nearest .git directory, returns its parent. */
Expand All @@ -110,8 +161,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 +170,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 +179,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
162 changes: 162 additions & 0 deletions jmh-fork/jmh-core/src/main/java/io/codspeed/ClassFileSourceName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package io.codspeed;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/**
* 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 static final int MAGIC = 0xCAFEBABE;

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 (DataInputStream in = new DataInputStream(stream)) {
return parse(in);
} 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);
}

/** Parses a class file as specified by JVMS §4.1, stopping at the class-level attributes. */
private static String parse(DataInputStream in) throws IOException {
if (in.readInt() != MAGIC) {
return null;
}
in.readUnsignedShort(); // minor version
in.readUnsignedShort(); // major version

String[] utf8 = readConstantPool(in);
if (utf8 == null) {
return null;
}

in.readUnsignedShort(); // access flags
in.readUnsignedShort(); // this class
in.readUnsignedShort(); // super class
skipFully(in, 2 * in.readUnsignedShort()); // interfaces
skipMembers(in); // fields
skipMembers(in); // methods

int attributeCount = in.readUnsignedShort();
for (int i = 0; i < attributeCount; i++) {
int nameIndex = in.readUnsignedShort();
int length = in.readInt();
if ("SourceFile".equals(constant(utf8, nameIndex)) && length == 2) {
return constant(utf8, in.readUnsignedShort());
}
skipFully(in, length);
}
return null;
}

/** Collects the Utf8 entries of the constant pool, indexed by pool index. */
private static String[] readConstantPool(DataInputStream in) throws IOException {
int count = in.readUnsignedShort();
String[] utf8 = new String[Math.max(count, 1)];

for (int i = 1; i < count; i++) {
int tag = in.readUnsignedByte();
switch (tag) {
case 1: // Utf8
utf8[i] = in.readUTF();
break;
case 7: // Class
case 8: // String
case 16: // MethodType
case 19: // Module
case 20: // Package
skipFully(in, 2);
break;
case 15: // MethodHandle
skipFully(in, 3);
break;
case 3: // Integer
case 4: // Float
case 9: // Fieldref
case 10: // Methodref
case 11: // InterfaceMethodref
case 12: // NameAndType
case 17: // Dynamic
case 18: // InvokeDynamic
skipFully(in, 4);
break;
case 5: // Long
case 6: // Double
skipFully(in, 8);
// Long and Double occupy two constant pool slots
i++;
break;
default:
// Unknown tag: entry size is unknown, so the rest of the pool can't be walked
return null;
}
}
return utf8;
}

private static String constant(String[] utf8, int index) {
return index > 0 && index < utf8.length ? utf8[index] : null;
}

private static void skipMembers(DataInputStream in) throws IOException {
int count = in.readUnsignedShort();
for (int i = 0; i < count; i++) {
skipFully(in, 6); // access flags, name index, descriptor index
skipAttributes(in);
}
}

private static void skipAttributes(DataInputStream in) throws IOException {
int count = in.readUnsignedShort();
for (int i = 0; i < count; i++) {
in.readUnsignedShort(); // attribute name index
skipFully(in, in.readInt());
}
}

private static void skipFully(DataInputStream in, int count) throws IOException {
if (count < 0) {
throw new IOException("Invalid length: " + count);
}

int remaining = count;
while (remaining > 0) {
int skipped = in.skipBytes(remaining);
if (skipped <= 0) {
throw new EOFException();
}
remaining -= skipped;
}
}
}
Loading
Loading