Skip to content

New: Add INVOKEDYNAMIC instrumentation. #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions benchmarks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import com.intellij.rt.coverage.utils.ClassFileUpgradeTask

sourceSets {
jmh.java.srcDirs = [file('jmh')]
main.java.srcDirs = []
Expand All @@ -34,3 +36,13 @@ dependencies {
jmhImplementation 'junit:junit:4.13.1'
jmhImplementation fileTree('lib')
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

task upgradeJmhClasspath(type: ClassFileUpgradeTask) {
inputFiles = sourceSets.jmh.runtimeClasspath
outputDirectory = layout.buildDirectory.dir("jmhClasspath").get()
}
36 changes: 24 additions & 12 deletions benchmarks/jmh.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ task NoAgent(type: JavaExec) {
def configureBenchmark(JavaExec benchmark, Closure<List<String>> jvmArgs = {[]}) {
benchmark.with {
group = 'benchmarks'
dependsOn ":benchmarks:jmhClasses"
dependsOn(":benchmarks:jmhClasses", ":benchmarks:upgradeJmhClasspath")
main = 'org.openjdk.jmh.Main'
doFirst {
classpath = project(":benchmarks").sourceSets.jmh.runtimeClasspath
classpath = fileTree(project(":benchmarks").upgradeJmhClasspath.outputs.files.singleFile)
classpath += project(":benchmarks").sourceSets.jmh.output.classesDirs
args = [
'-jvmArgs', '-Dfile.encoding=UTF-8',
// benchmarks
Expand Down Expand Up @@ -73,27 +74,38 @@ def configureBenchmark(JavaExec benchmark, Closure<List<String>> jvmArgs = {[]})
}
}

ext.configureBenchmark = this.&configureBenchmark

def benchmarkReport(Task benchmark) {
file("$benchmark.temporaryDir/${benchmark.name}.json")
}


ext.configureCompareWith = { benchmark, Closure<List<String>> jvmArgs, basicBenchmark ->
configureBenchmark(benchmark, jvmArgs)
benchmark.with {
dependsOn(basicBenchmark)
ext.configureComparison = { combined, benchmark1, benchmark2 ->
combined.with {
group = 'benchmarks'
if (benchmark1 != combined) {
dependsOn benchmark1
}
if (benchmark2 != combined) {
dependsOn benchmark2
}
doLast {
def noAgentReport = ResourceGroovyMethods.getText(benchmarkReport(basicBenchmark), 'UTF-8')
def currentReport = ResourceGroovyMethods.getText(benchmarkReport(benchmark), 'UTF-8')
def firstReport = ResourceGroovyMethods.getText(benchmarkReport(benchmark1), 'UTF-8')
def secondReport = ResourceGroovyMethods.getText(benchmarkReport(benchmark2), 'UTF-8')

project.logger.quiet """Benchmark score:
$basicBenchmark.name vs $benchmark.name:
${ReportReader.readScore(noAgentReport, currentReport, secondaryMetrics)}
$benchmark1.name vs $benchmark2.name:
${ReportReader.readScore(firstReport, secondReport, secondaryMetrics)}
"""
}
}
}

ext.configureCompare = { benchmark, Closure<List<String>> jvmArgs ->
ext.configureCompareWith = { benchmark, Closure<List<String>> jvmArgs, basicBenchmark ->
configureBenchmark(benchmark, jvmArgs)
configureComparison(benchmark, basicBenchmark, benchmark)
}

ext.configureCompareWithNoAgent = { benchmark, Closure<List<String>> jvmArgs ->
configureCompareWith(benchmark, jvmArgs, NoAgent)
}
3 changes: 3 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ repositories {

dependencies {
implementation("com.guardsquare:proguard-gradle:7.3.0")
implementation("org.ow2.asm:asm-tree:9.7")
implementation("org.ow2.asm:asm-commons:9.7")
implementation("commons-io:commons-io:2.16.1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2000-2024 JetBrains s.r.o.
*
* Licensed 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 com.intellij.rt.coverage.utils

import groovy.transform.CompileStatic
import org.apache.commons.io.IOUtils
import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.objectweb.asm.ClassReader

import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

@CompileStatic
abstract class ClassFileUpgradeTask extends DefaultTask {
@InputFiles
FileCollection inputFiles

@OutputDirectory
Directory outputDirectory

@TaskAction
void transform() {
def inputJars = inputFiles.files.findAll { it.name.endsWith(".jar") }
List<ZipFile> zipFiles = []
try {
inputJars.each {
zipFiles << new ZipFile(it)
}
process(zipFiles)
} finally {
zipFiles.each { it.close() }
}
}

private void process(List<ZipFile> zipFiles) {
def superClasses = collectSuperClasses(zipFiles)
transformJars(zipFiles, superClasses)
}

private Map<String, String> collectSuperClasses(List<ZipFile> zipFiles) {
Map<String, String> superClasses = [:]
zipFiles.each { zip ->
eachEntry(zip) { ZipEntry classEntry ->
if (!classEntry.name.endsWith(".class")) {
return
}
zip.getInputStream(classEntry).withStream { inputStream ->
def reader = new ClassReader(inputStream)
superClasses[reader.className] = reader.superName
}
}
}
return superClasses
}

private void transformJars(List<ZipFile> zipFiles, Map<String, String> superClasses) {
zipFiles.each { zip ->
def outputFile = outputDirectory.file(new File(zip.name).name).asFile
new ZipOutputStream(outputFile.newOutputStream()).withStream { output ->
eachEntry(zip) { ZipEntry entry ->
output.putNextEntry(new ZipEntry(entry.name))
if (!entry.isDirectory()) {
zip.getInputStream(entry).withStream { input ->
if (entry.name.endsWith(".class")) {
def bytes = transformClass(input, superClasses)
IOUtils.write(bytes, output)
} else {
IOUtils.copy(input, output)
}
}
}
output.closeEntry()
}
}
}
}

private byte[] transformClass(InputStream stream, Map<String, String> superClasses) {
def reader = new ClassReader(stream)
def writer = new HierarchyClassWriter(superClasses)
reader.accept(new UpgradingClassVisitor(writer), 0)
return writer.toByteArray()
}

private void eachEntry(ZipFile zip, Closure<?> callback) {
zip.entries().iterator().each { ZipEntry entry ->
callback(entry)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2000-2024 JetBrains s.r.o.
*
* Licensed 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 com.intellij.rt.coverage.utils

import groovy.transform.CompileStatic
import org.objectweb.asm.ClassWriter

@CompileStatic
class HierarchyClassWriter extends ClassWriter {
private final Map<String, String> superClasses
private final Map<String, LinkedHashSet<String>> hierarchies = [:]

HierarchyClassWriter(Map<String, String> superClasses) {
super(COMPUTE_FRAMES)
this.superClasses = superClasses
}

@Override
protected String getCommonSuperClass(String type1, String type2) {
def hierarchy1 = getHierarchy(type1)
def hierarchy2 = getHierarchy(type2)
return hierarchy1.find { it in hierarchy2 }
}

private LinkedHashSet<String> getHierarchy(String type) {
LinkedHashSet<String> result = hierarchies[type]
if (result != null) {
return result
}
result = []
hierarchies[type] = result
while (type != null) {
result << type
type = getSuperClass(type)
}
result << "java/lang/Object"
return result
}

private String getSuperClass(String type) {
String known = superClasses[type]
if (known != null) {
return known
}
def clazz = Class.forName(type.replace('/', '.'), false, classLoader)
return clazz.superclass?.name?.replace('.', '/')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2000-2024 JetBrains s.r.o.
*
* Licensed 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 com.intellij.rt.coverage.utils

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.JSRInlinerAdapter

class UpgradingClassVisitor extends ClassVisitor {
private int version

protected UpgradingClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM9, classVisitor)
}

@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.version = version
if (version < Opcodes.V11) {
version = Opcodes.V11
}
super.visit(version, access, name, signature, superName, interfaces)
}

@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
def mv = super.visitMethod(access, name, descriptor, signature, exceptions)
if (version < Opcodes.V1_7) {
return new JSRInlinerAdapter(mv, access, name, descriptor, signature, exceptions)
}
return mv
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2000-2024 JetBrains s.r.o.
*
* Licensed 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 com.intellij.rt.coverage.util;

import com.intellij.rt.coverage.instrumentation.CoverageRuntime;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

@SuppressWarnings("unused")
public class IndyUtils {
public static CallSite getHits(MethodHandles.Lookup lookup, String name, MethodType type, String className) {
return constant(CoverageRuntime.getHits(className));
}

public static CallSite getHitsMask(MethodHandles.Lookup lookup, String name, MethodType type, String className) {
return constant(CoverageRuntime.getHitsMask(className));
}

public static CallSite getTraceMask(MethodHandles.Lookup lookup, String name, MethodType type, String className) {
return constant(CoverageRuntime.getTraceMask(className));
}

public static CallSite loadClassData(MethodHandles.Lookup lookup, String name, MethodType type, String className) {
return constant(CoverageRuntime.loadClassData(className));
}

private static CallSite constant(Object cst) {
return new ConstantCallSite(MethodHandles.constant(Object.class, cst));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ private CoverageDataAccess createDataAccess(String className, ClassReader cr) {
if (OptionsUtil.FIELD_INSTRUMENTATION_ENABLED) {
if (InstrumentationUtils.isCondyEnabled(cr)) {
return new CondyCoverageDataAccess(createCondyInit(className, cr));
} else if (InstrumentationUtils.isIndyEnabled(cr)) {
return new IndyCoverageDataAccess(createIndyInit(className, cr));
} else {
return new FieldCoverageDataAccess(cr, className, createInit(className, cr, false));
}
Expand All @@ -64,6 +66,17 @@ protected CoverageDataAccess.Init createInit(String className, ClassReader cr, b
methodName, "(Ljava/lang/String;)" + arrayType, new Object[]{className});
}

protected CoverageDataAccess.Init createIndyInit(String className, ClassReader cr) {
boolean calculateHits = myProjectContext.getOptions().isCalculateHits;
String arrayType = calculateHits ? DataAccessUtil.HITS_ARRAY_TYPE : DataAccessUtil.MASK_ARRAY_TYPE;
String methodName = calculateHits ? "getHits" : "getHitsMask";
return new CoverageDataAccess.Init(
"__$hits$__", arrayType, "com/intellij/rt/coverage/util/IndyUtils", methodName,
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;",
new Object[]{className}
);
}

protected CoverageDataAccess.Init createCondyInit(String className, ClassReader cr) {
boolean calculateHits = myProjectContext.getOptions().isCalculateHits;
String arrayType = calculateHits ? DataAccessUtil.HITS_ARRAY_TYPE : DataAccessUtil.MASK_ARRAY_TYPE;
Expand Down
Loading