aboutsummaryrefslogtreecommitdiff
path: root/agent/src/main/java/io
diff options
context:
space:
mode:
Diffstat (limited to 'agent/src/main/java/io')
-rw-r--r--agent/src/main/java/io/perfmark/agent/MethodVisitorRecorder.java435
-rw-r--r--agent/src/main/java/io/perfmark/agent/MethodWrappingWriter.java164
-rw-r--r--agent/src/main/java/io/perfmark/agent/NonMergingClassWriter.java39
-rw-r--r--agent/src/main/java/io/perfmark/agent/PerfMarkAgent.java31
-rw-r--r--agent/src/main/java/io/perfmark/agent/PerfMarkClassVisitor.java172
-rw-r--r--agent/src/main/java/io/perfmark/agent/PerfMarkMethodRewriter.java175
-rw-r--r--agent/src/main/java/io/perfmark/agent/PerfMarkTransformer.java184
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.
+ }
+}