diff options
Diffstat (limited to 'agent/src/main/java/io')
7 files changed, 1200 insertions, 0 deletions
diff --git a/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java b/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java new file mode 100644 index 0000000..960d0f7 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java @@ -0,0 +1,435 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypePath; + +/** + * This class records the "header" portion of the method visitor. + */ +class MethodVisitorRecorder extends MethodVisitor { + + private final int VISIT_PARAMETER = 1; + private final int VISIT_ANNOTATION_DEFAULT = 2; + + private final int ANNOTATION_VISIT = 3; + private final int ANNOTATION_VISIT_ENUM = 4; + private final int ANNOTATION_VISIT_ANNOTATION = 5; + private final int ANNOTATION_VISIT_ARRAY = 6; + private final int ANNOTATION_VISIT_END = 7; + + private final int VISIT_ANNOTATION = 8; + private final int VISIT_ANNOTABLE_PARAMETER_COUNT = 9; + private final int VISIT_PARAMETER_ANNOTATION = 10; + private final int VISIT_TYPE_ANNOTATION = 11; + private final int VISIT_ATTRIBUTE = 12; + + private final AnnotationVisitorRecorder annotationVisitorRecorder = new AnnotationVisitorRecorder(null); + + private int opsWidx; + private int intsWidx; + private int stringsWidx; + private int objectsWidx; + private int booleansWidx; + + private int[] ops = new int[0]; + private String[] strings = new String[0]; + private int[] ints = new int[0]; + private Object[] objects = new Object[0]; + private boolean[] booleans = new boolean[0]; + + int firstLine = -1; + int lastLine = -1; + + MethodVisitorRecorder(MethodVisitor delegate) { + // Have to pin to a specific version, since the method invocations may be different in a later release + super(Opcodes.ASM9, delegate); + } + + /* + * Docs for Method Visitor Say: + * + * A visitor to visit a Java method. The methods of this class must be called in the following order: + * ( visitParameter )* + * [ visitAnnotationDefault ] + * ( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )* + * [ visitCode ( + * visitFrame | visit<i>X</i>Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock + * | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* + * visitMaxs ] + * visitEnd. + * + * + * In addition, the visit<i>X</i>Insn and visitLabel methods must be called in the sequential order of the bytecode + * instructions of the visited code, visitInsnAnnotation must be called after the annotated instruction, + * visitTryCatchBlock must be called before the labels passed as arguments have been visited, + * visitTryCatchBlockAnnotation must be called after the corresponding try catch block has been visited, and the + * visitLocalVariable, visitLocalVariableAnnotation and visitLineNumber methods must be called after the labels passed + * as arguments have been visited. + */ + + @Override + public final void visitParameter(String name, int access) { + addOp(VISIT_PARAMETER); + addString(name); + addInt(access); + super.visitParameter(name, access); + } + + @Override + public final AnnotationVisitor visitAnnotationDefault() { + addOp(VISIT_ANNOTATION_DEFAULT); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitAnnotationDefault()); + } + + @Override + public final AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + addOp(VISIT_ANNOTATION); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitAnnotation(descriptor, visible)); + } + + @Override + public final void visitAnnotableParameterCount(int parameterCount, boolean visible) { + addOp(VISIT_ANNOTABLE_PARAMETER_COUNT); + addInt(parameterCount); + addBoolean(visible); + super.visitAnnotableParameterCount(parameterCount, visible); + } + + @Override + public final AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { + addOp(VISIT_PARAMETER_ANNOTATION); + addInt(parameter); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitParameterAnnotation(parameter, descriptor, visible)); + } + + @Override + public final AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String descriptor, boolean visible) { + addOp(VISIT_TYPE_ANNOTATION); + addInt(typeRef); + addObject(typePath); + addString(descriptor); + addBoolean(visible); + if (mv == null) { + return annotationVisitorRecorder; + } + return new AnnotationVisitorRecorder(super.visitTypeAnnotation(typeRef, typePath, descriptor, visible)); + } + + @Override + public final void visitAttribute(Attribute attribute) { + addOp(VISIT_ATTRIBUTE); + addObject(attribute); + super.visitAttribute(attribute); + } + + @Override + public final void visitLineNumber(int line, Label start) { + if (firstLine == -1) { + firstLine = line; + } + lastLine = line; + super.visitLineNumber(line, start); + } + + final void replay() { + if (mv != null) { + replay(mv); + } + } + + final void replay(MethodVisitor delegate) { + if (delegate == null) { + return; + } + int stringsRidx = 0; + int intsRidx = 0; + int objectsRidx = 0; + int booleansRidx = 0; + int annoWidx = 0; + AnnotationVisitor[] visitorStack = new AnnotationVisitor[0]; + for (int opsRidx = 0; opsRidx < opsWidx; opsRidx++) { + int op = ops[opsRidx]; + switch (op) { + case VISIT_PARAMETER: { + String name = getString(stringsRidx++); + int access = getInt(intsRidx++); + delegate.visitParameter(name, access); + break; + } + case VISIT_ANNOTATION_DEFAULT: { + AnnotationVisitor visitor = delegate.visitAnnotationDefault(); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, visitor); + break; + } + case ANNOTATION_VISIT: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + Object value = getObject(objectsRidx++); + if (currentVisitor != null) { + currentVisitor.visit(name, value); + } + break; + } + case ANNOTATION_VISIT_ENUM: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + String descriptor = getString(stringsRidx++); + String value = getString(stringsRidx++); + if (currentVisitor != null) { + currentVisitor.visitEnum(name, descriptor, value); + } + break; + } + case ANNOTATION_VISIT_ANNOTATION: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + String descriptor = getString(stringsRidx++); + AnnotationVisitor newVisitor = null; + if (currentVisitor != null) { + newVisitor = currentVisitor.visitAnnotation(name, descriptor); + } + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case ANNOTATION_VISIT_ARRAY: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + String name = getString(stringsRidx++); + AnnotationVisitor newVisitor = null; + if (currentVisitor != null) { + newVisitor = currentVisitor.visitArray(name); + } + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case ANNOTATION_VISIT_END: { + AnnotationVisitor currentVisitor = visitorStack[annoWidx - 1]; + visitorStack[--annoWidx] = null; + if (currentVisitor != null) { + currentVisitor.visitEnd(); + } + break; + } + case VISIT_ANNOTATION: { + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitAnnotation(descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_ANNOTABLE_PARAMETER_COUNT: { + int parameterCount = getInt(intsRidx++); + boolean visible = getBoolean(booleansRidx++); + delegate.visitAnnotableParameterCount(parameterCount, visible); + break; + } + case VISIT_PARAMETER_ANNOTATION: { + int parameter = getInt(intsRidx++); + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitParameterAnnotation(parameter, descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_TYPE_ANNOTATION: { + // (int typeRef, TypePath typePath, String descriptor, boolean visible) + int typeRef = getInt(intsRidx++); + TypePath typePath = (TypePath) getObject(objectsRidx++); + String descriptor = getString(stringsRidx++); + boolean visible = getBoolean(booleansRidx++); + AnnotationVisitor newVisitor = delegate.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + visitorStack = addAnnotationVisitor(visitorStack, annoWidx++, newVisitor); + break; + } + case VISIT_ATTRIBUTE: { + Attribute attribute = (Attribute) getObject(objectsRidx++); + delegate.visitAttribute(attribute); + break; + } + default: + throw new AssertionError("Bad op " + op); + } + } + } + + private void addOp(int op) { + ops = addInt(ops, opsWidx++, op); + } + + private void addInt(int value) { + ints = addInt(ints, intsWidx++, value); + } + + private static int[] addInt(int[] dest, int pos, int value) { + if (dest.length == pos){ + int[] newDest = new int[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addString(String value) { + strings = addString(strings, stringsWidx++, value); + } + + private static String[] addString(String[] dest, int pos, String value) { + if (dest.length == pos){ + String[] newDest = new String[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addObject(Object value) { + objects = addObject(objects, objectsWidx++, value); + } + + private static Object[] addObject(Object[] dest, int pos, Object value) { + if (dest.length == pos){ + Object[] newDest = new Object[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private void addBoolean(boolean value) { + booleans = addBoolean(booleans, booleansWidx++, value); + } + + private static boolean[] addBoolean(boolean[] dest, int pos, boolean value) { + if (dest.length == pos){ + boolean[] newDest = new boolean[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private static AnnotationVisitor[] addAnnotationVisitor(AnnotationVisitor[] dest, int pos, AnnotationVisitor value) { + if (dest.length == pos){ + AnnotationVisitor[] newDest = new AnnotationVisitor[1 + dest.length * 2]; + System.arraycopy(dest, 0, newDest, 0, dest.length); + dest = newDest; + } + dest[pos] = value; + return dest; + } + + private int getInt(int ridx) { + assert ridx < intsWidx; + return ints[ridx]; + } + + private String getString(int ridx) { + assert ridx < stringsWidx; + return strings[ridx]; + } + + private Object getObject(int ridx) { + assert ridx < objectsWidx; + return objects[ridx]; + } + + private boolean getBoolean(int ridx) { + assert ridx < booleansWidx; + return booleans[ridx]; + } + + private final class AnnotationVisitorRecorder extends AnnotationVisitor { + + AnnotationVisitorRecorder(AnnotationVisitor delegate) { + super(MethodVisitorRecorder.this.api, delegate); + } + + /* + * Docs for AnnotationVisitor say + * + * A visitor to visit a Java annotation. The methods of this class must be called in the following order: + * ( visit | visitEnum | visitAnnotation | visitArray )* visitEnd. + */ + + @Override + public void visit(String name, Object value) { + addOp(ANNOTATION_VISIT); + addString(name); + addObject(value); + super.visit(name, value); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + addOp(ANNOTATION_VISIT_ENUM); + addString(name); + addString(descriptor); + addString(value); + super.visitEnum(name, descriptor, value); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String descriptor) { + addOp(ANNOTATION_VISIT_ANNOTATION); + addString(name); + addString(descriptor); + if (av == null) { + return this; + } + return new AnnotationVisitorRecorder(super.visitAnnotation(name, descriptor)); + } + + @Override + public AnnotationVisitor visitArray(String name) { + addOp(ANNOTATION_VISIT_ARRAY); + addString(name); + if (av == null) { + return this; + } + return new AnnotationVisitorRecorder(super.visitArray(name)); + } + + @Override + public void visitEnd() { + addOp(ANNOTATION_VISIT_END); + super.visitEnd(); + } + } +} diff --git a/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java b/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java new file mode 100644 index 0000000..46dd6e2 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +final class MethodWrappingWriter { + + private final MethodVisitorRecorder recorder; + private final int access; + private final String methodName; + private final String descriptor; + private final String signature; + private final String[] exceptions; + private final String className; + private final String bodyMethodName; + private final ClassVisitor classVisitor; + private final boolean isInterface; + + MethodWrappingWriter( + MethodVisitorRecorder recorder, int access, String methodName, String descriptor, String signature, + String[] exceptions, boolean isInterface, String className, String bodyMethodName, ClassVisitor cv) { + this.recorder = recorder; + this.access = access; + this.methodName = methodName; + this.descriptor = descriptor; + this.signature = signature; + this.exceptions = exceptions; + this.isInterface = isInterface; + this.className = className; + this.bodyMethodName = bodyMethodName; + this.classVisitor = cv; + } + + void visit() { + MethodVisitor mv = classVisitor.visitMethod(access, methodName, descriptor, signature, exceptions); + if (mv == null) { + return; + } + // TODO(carl-mastrangelo): add start / stop call tags here + recorder.replay(mv); + + mv.visitCode(); + Label start = new Label(); + Label end = new Label(); + mv.visitLabel(start); + mv.visitLineNumber(recorder.firstLine, start); + mv.visitLdcInsn(className + "::" + methodName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "io/perfmark/PerfMark", "startTask", "(Ljava/lang/String;)V", false); + + if (((access & Opcodes.ACC_STATIC) == 0)) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + } + + int params = 1; + char ret = 0; + assert descriptor.charAt(0) == '('; + out: for (int i = 1; i < descriptor.length(); i++) { + char c = descriptor.charAt(i); + switch (c) { + case ')': + ret = descriptor.charAt(i + 1); + break out; + case 'L': + mv.visitVarInsn(Opcodes.ALOAD, params++); + i = descriptor.indexOf(';', i); + assert i > 0; + break; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitVarInsn(Opcodes.ILOAD, params++); + break; + case 'F': + mv.visitVarInsn(Opcodes.FLOAD, params++); + break; + case 'J': + mv.visitVarInsn(Opcodes.LLOAD, params++); + break; + case 'D': + mv.visitVarInsn(Opcodes.DLOAD, params++); + break; + case '[': + mv.visitVarInsn(Opcodes.ALOAD, params++); + while (descriptor.charAt(++i) == '[') { + // empty body on purpose. + } + if (descriptor.charAt(i) == 'L') { + i = descriptor.indexOf(';', i); + } + + break; + default: + throw new RuntimeException("Bad descriptor " + c + " in " + descriptor); + } + } + + int invoke; + if ((access & Opcodes.ACC_STATIC) != 0) { + invoke = Opcodes.INVOKESTATIC; + } else if (isInterface) { + invoke = Opcodes.INVOKEINTERFACE; + } else { + invoke = Opcodes.INVOKESPECIAL; + } + + mv.visitMethodInsn(invoke, className.replace(".", "/"), bodyMethodName, descriptor, isInterface); + + mv.visitLabel(end); + mv.visitLineNumber(recorder.lastLine, end); + mv.visitLdcInsn(className + ":::" + methodName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "io/perfmark/PerfMark", "stopTask", "(Ljava/lang/String;)V", false); + + switch (ret) { + case 'V': + mv.visitInsn(Opcodes.RETURN); + break; + case 'L': + case '[': + mv.visitInsn(Opcodes.ARETURN); + break; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + mv.visitInsn(Opcodes.IRETURN); + break; + case 'F': + mv.visitInsn(Opcodes.FRETURN); + break; + case 'J': + mv.visitInsn(Opcodes.LRETURN); + break; + case 'D': + mv.visitInsn(Opcodes.DRETURN); + break; + default: + throw new RuntimeException("Bad Descriptor " + ret); + } + + mv.visitMaxs(params, params + 1); + mv.visitEnd(); + } +} diff --git a/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java b/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java new file mode 100644 index 0000000..c93c648 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +/** + * This class exists to avoid the runtime lookup of classes when doing flow analysis. When + * computing frames, the default implementation needs to merge types together, which involves + * finding their common super class. This makes the transformer accidentally load a bunch of + * classes before they can be transformed. To avoid the possibility of this happening this + * class writer intentionally fails any such attempts. This ensures flags like {@link + * ClassWriter#COMPUTE_FRAMES} don't trigger merge logic. + */ +final class NonMergingClassWriter extends ClassWriter { + NonMergingClassWriter(ClassReader classReader, int flags) { + super(classReader, flags); + } + + @Override + protected String getCommonSuperClass(String type1, String type2) { + throw new UnsupportedOperationException("avoiding reflective lookup of classes"); + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java b/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java new file mode 100644 index 0000000..ab4b2e9 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.agent; + +import java.lang.instrument.Instrumentation; + +public final class PerfMarkAgent { + + /** + * Entry point as an agent. + */ + public static void premain(String agentArgs, Instrumentation inst) { + inst.addTransformer(new PerfMarkTransformer(inst)); + } + + private PerfMarkAgent() {} +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java b/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java new file mode 100644 index 0000000..3833068 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.ModuleVisitor; +import org.objectweb.asm.Opcodes; + +final class PerfMarkClassVisitor extends ClassVisitor { + + static final String[] ALL_METHODS = new String[0]; + + private final Runnable changeDetector = new ChangeDetector(); + + private final String classLoaderName; + private final String className; + private final String[] methodsToRewrite; + private final String[] methodsToWrap; + + private String fileName; + private String moduleName; + private String moduleVersion; + private boolean isInterface; + @SuppressWarnings("unused") + private boolean madeChanges; + + PerfMarkClassVisitor( + String classLoaderName, String className, String[] methodsToRewrite, String[] methodsToWrap, + ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + this.classLoaderName = classLoaderName; + this.className = className; + this.methodsToRewrite = methodsToRewrite == ALL_METHODS ? ALL_METHODS : methodsToRewrite.clone(); + this.methodsToWrap = methodsToWrap == ALL_METHODS ? ALL_METHODS : methodsToWrap.clone(); + } + + @Override + public ModuleVisitor visitModule(String name, int access, String version) { + this.moduleName = name; + this.moduleVersion = version; + return super.visitModule(name, access, version); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitSource(String source, String debug) { + this.fileName = source; + super.visitSource(source, debug); + } + + @Override + public MethodVisitor visitMethod( + int access, String methodName, String descriptor, String signature, String[] exceptions) { + final MethodVisitor superDelegate; + final MethodWrappingWriter methodWrapper; + if (shouldWrap(methodName, access)) { + if (className.startsWith("io/perfmark/") || className.startsWith("jdk/")) { + // Avoid recursion for now. + return super.visitMethod(access, methodName, descriptor, signature, exceptions); + } + String bodyMethodName = methodName + "$perfmark"; + int newAccess; + if (!isInterface) { + newAccess = (access | Opcodes.ACC_PRIVATE) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); + } else { + newAccess = access; + } + MethodVisitor visitor = super.visitMethod(newAccess, bodyMethodName, descriptor, signature, exceptions); + MethodVisitorRecorder recorder = new MethodVisitorRecorder(visitor); + superDelegate = recorder; + methodWrapper = + new MethodWrappingWriter( + recorder, access, methodName, descriptor, signature, exceptions, isInterface, className, bodyMethodName, + cv); + } else { + superDelegate = super.visitMethod(access, methodName, descriptor, signature, exceptions); + methodWrapper = null; + } + + final MethodVisitor visitor; + if (shouldRewrite(methodName)) { + visitor = new PerfMarkMethodRewriter( + classLoaderName, moduleName, moduleVersion, className, methodName, fileName, changeDetector, superDelegate); + } else { + visitor = superDelegate; + } + + return new MethodVisitorWrapper(methodWrapper, visitor); + } + + boolean shouldWrap(String methodName, int access) { + if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) { + return false; + } + if (methodsToWrap == ALL_METHODS) { + // not yet supported + if (methodName.equals("<init>") || methodName.equals("<clinit>")) { + return false; + } + return true; + } + for (String method : methodsToWrap) { + if (method.equals(methodName)) { + return true; + } + } + return false; + } + + boolean shouldRewrite(String methodName) { + if (methodsToRewrite == ALL_METHODS) { + return true; + } + for (String method : methodsToRewrite) { + if (method.equals(methodName)) { + return true; + } + } + return false; + } + + private final class MethodVisitorWrapper extends MethodVisitor { + + private final MethodWrappingWriter methodWrapper; + + /** + * + * @param methodWrapper the writer of the "wrapper" method. May be {@code null}. + * @param delegate method visitor to inject in the additional method. May be {@code null}. + */ + MethodVisitorWrapper(MethodWrappingWriter methodWrapper, MethodVisitor delegate) { + super(PerfMarkClassVisitor.this.api, delegate); + this.methodWrapper = methodWrapper; + } + + @Override + public void visitEnd() { + super.visitEnd(); + if (methodWrapper != null) { + madeChanges = true; + methodWrapper.visit(); + } + } + } + + private final class ChangeDetector implements Runnable { + @Override + public void run() { + madeChanges = true; + } + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java b/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java new file mode 100644 index 0000000..8a876b6 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java @@ -0,0 +1,175 @@ +/* + * Copyright 2021 Carl Mastrangelo + * + * 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 io.perfmark.agent; + +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Modified PerfMark startTask and stopTask call sits to include tags about where in code they came from. + */ +final class PerfMarkMethodRewriter extends MethodVisitor { + + private static final String PERFMARK_CLZ = "io/perfmark/PerfMark"; + private static final String TASKCLOSEABLE_CLZ = "io/perfmark/TaskCloseable"; + + /** + * May be {@code null} if absent. + */ + private static final Constructor<? extends StackTraceElement> MODULE_CTOR = getStackTraceElementCtorSafe(); + + private final Runnable onChange; + private final String classLoaderName; + private final String moduleName; + private final String moduleVersion; + // The internal fully qualified name. (e.g. java/lang/String) + private final String className; + private final String methodName; + private final String fileName; + + private int lineNumber = -1; + + /** + * Builds the rewriter to add debug into to trace calls. + * + * @param classLoaderName the loader of this class. May be {@code null}. + * @param moduleName the module of this class. May be {@code null}. + * @param moduleVersion the module version of this class. May be {@code null}. + * @param className the class that contains this method. Must be non-{@code null}. + * @param methodName the method currently being visited. Must be non-{@code null}. + * @param fileName the class that contains this method. May be {@code null}. + * @param onChange runnable to invoke if any changes are made to the class definition. May be {@code null}. + * @param methodVisitor the delegate to call. May be {@code null}. + */ + PerfMarkMethodRewriter( + String classLoaderName, String moduleName, String moduleVersion, String className, String methodName, + String fileName, Runnable onChange, MethodVisitor methodVisitor) { + super(Opcodes.ASM9, methodVisitor); + this.classLoaderName = classLoaderName; + this.moduleName = moduleName; + this.moduleVersion = moduleVersion; + this.className = className; + this.methodName = methodName; + this.fileName = fileName; + this.onChange = onChange; + } + + @Override + public void visitLineNumber(int line, Label start) { + lineNumber = line; + super.visitLineNumber(line, start); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (shouldPreTag(opcode, owner, name)) { + visitLdcInsn("PerfMark.stopCallSite"); + visitLdcInsn(callSite()); + super.visitMethodInsn(INVOKESTATIC, PERFMARK_CLZ, "attachTag", "(Ljava/lang/String;Ljava/lang/String;)V", false); + if (onChange != null) { + onChange.run(); + } + } + + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + + if (shouldPostTag(opcode, owner, name)) { + visitLdcInsn("PerfMark.startCallSite"); + visitLdcInsn(callSite()); + super.visitMethodInsn(INVOKESTATIC, PERFMARK_CLZ, "attachTag", "(Ljava/lang/String;Ljava/lang/String;)V", false); + if (onChange != null) { + onChange.run(); + } + } + } + + private boolean shouldPreTag(int opcode, String owner, String methodName) { + switch (opcode) { + case INVOKESTATIC: + return PERFMARK_CLZ.equals(owner) && methodName.equals("stopTask") && !TASKCLOSEABLE_CLZ.equals(className); + case INVOKEVIRTUAL: + return TASKCLOSEABLE_CLZ.equals(owner) && methodName.equals("close"); + default: + return false; + } + } + + private boolean shouldPostTag(int opcode, String owner, String methodName) { + return opcode == INVOKESTATIC + && PERFMARK_CLZ.equals(owner) + && (methodName.equals("startTask") || methodName.equals("traceTask")); + } + + private String callSite() { + StackTraceElement elem = null; + try { + elem = moduleElement(); + } catch (Throwable t) { + // TODO(carl-mastrangelo): this should log. + } + if (elem == null) { + elem = new StackTraceElement(className, methodName, fileName, lineNumber); + } + return elem.toString(); + } + + /** + * Builds the current callsite. Visible for testing. + * + * @return stack trace element using the modern constructor, or {@code null} if absent. + */ + StackTraceElement moduleElement() throws InvocationTargetException, InstantiationException, IllegalAccessException { + StackTraceElement elem = null; + if (MODULE_CTOR != null) { + elem = MODULE_CTOR.newInstance( + classLoaderName, moduleName, moduleVersion, className, methodName, fileName, lineNumber); + } + return elem; + } + + private static Constructor<StackTraceElement> getStackTraceElementCtorSafe() { + try { + return getStackTraceElementCtor(); + } catch (Throwable t) { + return null; + } + } + + /** + * Gets the more modern constructor. Visible for testing. + * + * @return the module-based constructor, or {@code null} if it is absent. + */ + static Constructor<StackTraceElement> getStackTraceElementCtor() { + Constructor<StackTraceElement> ctor = null; + try { + ctor = StackTraceElement.class.getConstructor( + String.class, String.class, String.class, String.class, String.class, String.class, int.class); + } catch (NoSuchMethodException e) { + // normal on JDK 8 and below, but include an assert in case the descriptor was wrong. + assert StackTraceElement.class.getConstructors().length < 2 : e; + } + return ctor; + } +} diff --git a/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java b/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java new file mode 100644 index 0000000..e5ce204 --- /dev/null +++ b/agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Google LLC + * + * 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 io.perfmark.agent; + +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.ProtectionDomain; +import java.util.jar.JarFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +final class PerfMarkTransformer implements ClassFileTransformer { + + /** May be {@code null}. */ + private static final Method CLASS_LOADER_GET_NAME = getClassLoaderNameMethodSafe(); + + @SuppressWarnings("unused") + private final Instrumentation instrumentation; + + /** + * @param instrumentation may be {@code null}. + */ + PerfMarkTransformer(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + if (instrumentation != null) { + try { + URL url = getClass().getClassLoader().getResource("io/perfmark/PerfMark.class"); + int index; + if (url != null && "jar".equals(url.getProtocol()) && (index = url.getFile().indexOf("!/")) != -1) { + // The "file" is what appendToBootstrapClassLoaderSearch uses, so we will too. The jar format + // looks like "file:/home/jars/perfmark-api.jar:/io/perfmark/PerfMark.class", so we need to strip + // off the "file:" and the bangslash. + JarFile jf = new JarFile(url.getFile().substring(5, index)); + jf.close(); + instrumentation.appendToBootstrapClassLoaderSearch(jf); + } + } catch (IOException e ) { + throw new RuntimeException(e); + } + } + } + + + @Override + public byte[] transform( + ClassLoader loader, + final String className, + Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + System.err.println(" Attempting " + className); + try { + return transformInternal(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } catch (Exception e) { + System.err.println(e.toString()); + e.printStackTrace(System.err); + throw new RuntimeException(e); + } + } + + byte[] transformInternal( + ClassLoader loader, + final String className, + Class<?> classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + assert !className.contains(".") : "Binary name with `.` detected rather than internal name"; + String classLoaderName = getClassLoaderName(loader); + return transform(classLoaderName, className, classfileBuffer); + } + + private static byte[] transform(String classLoaderName, String className, byte[] classfileBuffer) { + ClassReader cr = new ClassReader(classfileBuffer); + if (true) { + ClassWriter cw = new NonMergingClassWriter(cr, ClassWriter.COMPUTE_MAXS); + PerfMarkClassVisitor perfMarkClassVisitor = + new PerfMarkClassVisitor( + classLoaderName, className, PerfMarkClassVisitor.ALL_METHODS, new String[0], cw); + cr.accept(perfMarkClassVisitor, 0); + return cw.toByteArray(); + } + return null; + } + + static String deriveFileName(String className) { + String clzName = className.replace('/', '.'); + int dollar = clzName.indexOf('$'); + String fileName; + if (dollar == -1) { + fileName = clzName; + } else { + fileName = clzName.substring(0, dollar); + } + if (!fileName.isEmpty()) { + int dot = fileName.lastIndexOf('.'); + if (dot != -1) { + fileName = fileName.substring(dot + 1); + } + } + // TODO: this is broken for private top level classes. + if (!fileName.isEmpty()) { + fileName += ".java"; + } else { + fileName = null; + } + return fileName; + } + + /** + * Returns the name for the class loader. Visible for testing. + * + * @param loader the class loader. May be {@code null}. + * @return The name of the class loader, or {@code null} if unavailable + */ + static String getClassLoaderName(ClassLoader loader) { + if (loader == null) { + return null; + } + if (CLASS_LOADER_GET_NAME != null) { + try { + return (String) CLASS_LOADER_GET_NAME.invoke(loader); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Error) { + throw (Error) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + return null; + } + + private static Method getClassLoaderNameMethodSafe() { + try { + return getClassLoaderNameMethod(); + } catch (Throwable t) { + safeLog(t, "Can't get loader method"); + } + return null; + } + + /** + * Gets name method for the Class loader for JDK9+. Visible for testing. + * + * @return the {@code getName} method, or {@code null} if it is absent. + */ + static Method getClassLoaderNameMethod() { + try { + return ClassLoader.class.getMethod("getName"); + } catch (NoSuchMethodException e) { + safeLog(e, "getName method missing"); + // expected + } + return null; + } + + @SuppressWarnings("UnusedVariable") + private static void safeLog(Throwable t, String message, Object... args) { + // TODO(carl-mastrangelo): implement. + } +} |