/*
 * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.jfr.internal;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Label;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.Method;
import jdk.internal.org.objectweb.asm.tree.AnnotationNode;
import jdk.internal.org.objectweb.asm.tree.ClassNode;
import jdk.internal.org.objectweb.asm.tree.FieldNode;
import jdk.internal.org.objectweb.asm.tree.MethodNode;
import jdk.jfr.Enabled;
import jdk.jfr.Event;
import jdk.jfr.Name;
import jdk.jfr.Registered;
import jdk.jfr.SettingControl;
import jdk.jfr.SettingDefinition;
import jdk.jfr.internal.handlers.EventHandler;

Class responsible for adding instrumentation to a subclass of Event.
/** * Class responsible for adding instrumentation to a subclass of {@link Event}. * */
public final class EventInstrumentation { static final class SettingInfo { private String methodName; private String internalSettingName; private String settingDescriptor; final String fieldName; final int index; // Used when instantiating Setting SettingControl settingControl; public SettingInfo(String fieldName, int index) { this.fieldName = fieldName; this.index = index; } } static final class FieldInfo { private final static Type STRING = Type.getType(String.class); final String fieldName; final String fieldDescriptor; final String internalClassName; public FieldInfo(String fieldName, String fieldDescriptor, String internalClassName) { this.fieldName = fieldName; this.fieldDescriptor = fieldDescriptor; this.internalClassName = internalClassName; } public boolean isString() { return STRING.getDescriptor().equals(fieldDescriptor); } } public static final String FIELD_EVENT_THREAD = "eventThread"; public static final String FIELD_STACK_TRACE = "stackTrace"; public static final String FIELD_DURATION = "duration"; static final String FIELD_EVENT_HANDLER = "eventHandler"; static final String FIELD_START_TIME = "startTime"; private static final Type ANNOTATION_TYPE_NAME = Type.getType(Name.class); private static final Type ANNOTATION_TYPE_REGISTERED = Type.getType(Registered.class); private static final Type ANNOTATION_TYPE_ENABLED = Type.getType(Enabled.class); private static final Type TYPE_EVENT_HANDLER = Type.getType(EventHandler.class); private static final Type TYPE_SETTING_CONTROL = Type.getType(SettingControl.class); private static final Type TYPE_OBJECT = Type.getType(Object.class); private static final Method METHOD_COMMIT = new Method("commit", Type.VOID_TYPE, new Type[0]); private static final Method METHOD_BEGIN = new Method("begin", Type.VOID_TYPE, new Type[0]); private static final Method METHOD_END = new Method("end", Type.VOID_TYPE, new Type[0]); private static final Method METHOD_IS_ENABLED = new Method("isEnabled", Type.BOOLEAN_TYPE, new Type[0]); private static final Method METHOD_TIME_STAMP = new Method("timestamp", Type.LONG_TYPE, new Type[0]); private static final Method METHOD_EVENT_SHOULD_COMMIT = new Method("shouldCommit", Type.BOOLEAN_TYPE, new Type[0]); private static final Method METHOD_EVENT_HANDLER_SHOULD_COMMIT = new Method("shouldCommit", Type.BOOLEAN_TYPE, new Type[] { Type.LONG_TYPE }); private static final Method METHOD_DURATION = new Method("duration", Type.LONG_TYPE, new Type[] { Type.LONG_TYPE }); private final ClassNode classNode; private final List<SettingInfo> settingInfos; private final List<FieldInfo> fieldInfos;; private final Method writeMethod; private final String eventHandlerXInternalName; private final String eventName; private final boolean untypedEventHandler; private boolean guardHandlerReference; private Class<?> superClass; EventInstrumentation(Class<?> superClass, byte[] bytes, long id) { this.superClass = superClass; this.classNode = createClassNode(bytes); this.settingInfos = buildSettingInfos(superClass, classNode); this.fieldInfos = buildFieldInfos(superClass, classNode); this.untypedEventHandler = hasUntypedHandler(); this.writeMethod = makeWriteMethod(fieldInfos); this.eventHandlerXInternalName = ASMToolkit.getInternalName(EventHandlerCreator.makeEventHandlerName(id)); String n = annotationValue(classNode, ANNOTATION_TYPE_NAME.getDescriptor(), String.class); this.eventName = n == null ? classNode.name.replace("/", ".") : n; } private boolean hasUntypedHandler() { for (FieldNode field : classNode.fields) { if (FIELD_EVENT_HANDLER.equals(field.name)) { return field.desc.equals(TYPE_OBJECT.getDescriptor()); } } throw new InternalError("Class missing handler field"); } public String getClassName() { return classNode.name.replace("/","."); } private ClassNode createClassNode(byte[] bytes) { ClassNode classNode = new ClassNode(); ClassReader classReader = new ClassReader(bytes); classReader.accept(classNode, 0); return classNode; } boolean isRegistered() { Boolean result = annotationValue(classNode, ANNOTATION_TYPE_REGISTERED.getDescriptor(), Boolean.class); if (result != null) { return result.booleanValue(); } if (superClass != null) { Registered r = superClass.getAnnotation(Registered.class); if (r != null) { return r.value(); } } return true; } boolean isEnabled() { Boolean result = annotationValue(classNode, ANNOTATION_TYPE_ENABLED.getDescriptor(), Boolean.class); if (result != null) { return result.booleanValue(); } if (superClass != null) { Enabled e = superClass.getAnnotation(Enabled.class); if (e != null) { return e.value(); } } return true; } @SuppressWarnings("unchecked") private static <T> T annotationValue(ClassNode classNode, String typeDescriptor, Class<?> type) { if (classNode.visibleAnnotations != null) { for (AnnotationNode a : classNode.visibleAnnotations) { if (typeDescriptor.equals(a.desc)) { List<Object> values = a.values; if (values != null && values.size() == 2) { Object key = values.get(0); Object value = values.get(1); if (key instanceof String && value != null) { if (type == value.getClass()) { String keyName = (String) key; if ("value".equals(keyName)) { return (T) value; } } } } } } } return null; } private static List<SettingInfo> buildSettingInfos(Class<?> superClass, ClassNode classNode) { Set<String> methodSet = new HashSet<>(); List<SettingInfo> settingInfos = new ArrayList<>(); String settingDescriptor = Type.getType(SettingDefinition.class).getDescriptor(); for (MethodNode m : classNode.methods) { if (m.visibleAnnotations != null) { for (AnnotationNode an : m.visibleAnnotations) { // We can't really validate the method at this // stage. We would need to check that the parameter // is an instance of SettingControl. if (settingDescriptor.equals(an.desc)) { Type returnType = Type.getReturnType(m.desc); if (returnType.equals(Type.getType(Boolean.TYPE))) { Type[] args = Type.getArgumentTypes(m.desc); if (args.length == 1) { Type paramType = args[0]; String fieldName = EventControl.FIELD_SETTING_PREFIX + settingInfos.size(); int index = settingInfos.size(); SettingInfo si = new SettingInfo(fieldName, index); si.methodName = m.name; si.settingDescriptor = paramType.getDescriptor(); si.internalSettingName = paramType.getInternalName(); methodSet.add(m.name); settingInfos.add(si); } } } } } } for (Class<?> c = superClass; c != jdk.internal.event.Event.class; c = c.getSuperclass()) { for (java.lang.reflect.Method method : c.getDeclaredMethods()) { if (!methodSet.contains(method.getName())) { // skip private method in base classes if (!Modifier.isPrivate(method.getModifiers())) { if (method.getReturnType().equals(Boolean.TYPE)) { if (method.getParameterCount() == 1) { Parameter param = method.getParameters()[0]; Type paramType = Type.getType(param.getType()); String fieldName = EventControl.FIELD_SETTING_PREFIX + settingInfos.size(); int index = settingInfos.size(); SettingInfo si = new SettingInfo(fieldName, index); si.methodName = method.getName(); si.settingDescriptor = paramType.getDescriptor(); si.internalSettingName = paramType.getInternalName(); methodSet.add(method.getName()); settingInfos.add(si); } } } } } } return settingInfos; } private static List<FieldInfo> buildFieldInfos(Class<?> superClass, ClassNode classNode) { Set<String> fieldSet = new HashSet<>(); List<FieldInfo> fieldInfos = new ArrayList<>(classNode.fields.size()); // These two field are added by native as transient so they will be // ignored by the loop below. // The benefit of adding them manually is that we can // control in which order they occur and we can add @Name, @Description // in Java, instead of in native. It also means code for adding implicit // fields for native can be reused by Java. fieldInfos.add(new FieldInfo("startTime", Type.LONG_TYPE.getDescriptor(), classNode.name)); fieldInfos.add(new FieldInfo("duration", Type.LONG_TYPE.getDescriptor(), classNode.name)); for (FieldNode field : classNode.fields) { if (!fieldSet.contains(field.name) && isValidField(field.access, Type.getType(field.desc).getClassName())) { FieldInfo fi = new FieldInfo(field.name, field.desc, classNode.name); fieldInfos.add(fi); fieldSet.add(field.name); } } for (Class<?> c = superClass; c != jdk.internal.event.Event.class; c = c.getSuperclass()) { for (Field field : c.getDeclaredFields()) { // skip private field in base classes if (!Modifier.isPrivate(field.getModifiers())) { if (isValidField(field.getModifiers(), field.getType().getName())) { String fieldName = field.getName(); if (!fieldSet.contains(fieldName)) { Type fieldType = Type.getType(field.getType()); String internalClassName = ASMToolkit.getInternalName(c.getName()); fieldInfos.add(new FieldInfo(fieldName, fieldType.getDescriptor(), internalClassName)); fieldSet.add(fieldName); } } } } } return fieldInfos; } public static boolean isValidField(int access, String className) { if (Modifier.isTransient(access) || Modifier.isStatic(access)) { return false; } return jdk.jfr.internal.Type.isValidJavaFieldType(className); } public byte[] buildInstrumented() { makeInstrumented(); return toByteArray(); } private byte[] toByteArray() { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); classNode.accept(cw); cw.visitEnd(); byte[] result = cw.toByteArray(); Utils.writeGeneratedASM(classNode.name, result); return result; } public byte[] builUninstrumented() { makeUninstrumented(); return toByteArray(); } private void makeInstrumented() { // MyEvent#isEnabled() updateMethod(METHOD_IS_ENABLED, methodVisitor -> { Label nullLabel = new Label(); if (guardHandlerReference) { getEventHandler(methodVisitor); methodVisitor.visitJumpInsn(Opcodes.IFNULL, nullLabel); } getEventHandler(methodVisitor); ASMToolkit.invokeVirtual(methodVisitor, TYPE_EVENT_HANDLER.getInternalName(), METHOD_IS_ENABLED); methodVisitor.visitInsn(Opcodes.IRETURN); if (guardHandlerReference) { methodVisitor.visitLabel(nullLabel); methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); methodVisitor.visitInsn(Opcodes.ICONST_0); methodVisitor.visitInsn(Opcodes.IRETURN); } }); // MyEvent#begin() updateMethod(METHOD_BEGIN, methodVisitor -> { methodVisitor.visitIntInsn(Opcodes.ALOAD, 0); ASMToolkit.invokeStatic(methodVisitor, TYPE_EVENT_HANDLER.getInternalName(), METHOD_TIME_STAMP); methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, getInternalClassName(), FIELD_START_TIME, "J"); methodVisitor.visitInsn(Opcodes.RETURN); }); // MyEvent#end() updateMethod(METHOD_END, methodVisitor -> { methodVisitor.visitIntInsn(Opcodes.ALOAD, 0); methodVisitor.visitIntInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, getInternalClassName(), FIELD_START_TIME, "J"); ASMToolkit.invokeStatic(methodVisitor, TYPE_EVENT_HANDLER.getInternalName(), METHOD_DURATION); methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, getInternalClassName(), FIELD_DURATION, "J"); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(0, 0); }); // MyEvent#commit() - Java event writer updateMethod(METHOD_COMMIT, methodVisitor -> { // if (!isEnable()) { // return; // } methodVisitor.visitCode(); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, getInternalClassName(), METHOD_IS_ENABLED.getName(), METHOD_IS_ENABLED.getDescriptor(), false); Label l0 = new Label(); methodVisitor.visitJumpInsn(Opcodes.IFNE, l0); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitLabel(l0); methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); // if (startTime == 0) { // startTime = EventWriter.timestamp(); // } else { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, getInternalClassName(), FIELD_START_TIME, "J"); methodVisitor.visitInsn(Opcodes.LCONST_0); methodVisitor.visitInsn(Opcodes.LCMP); Label durationalEvent = new Label(); methodVisitor.visitJumpInsn(Opcodes.IFNE, durationalEvent); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, TYPE_EVENT_HANDLER.getInternalName(), METHOD_TIME_STAMP.getName(), METHOD_TIME_STAMP.getDescriptor(), false); methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, getInternalClassName(), FIELD_START_TIME, "J"); Label commit = new Label(); methodVisitor.visitJumpInsn(Opcodes.GOTO, commit); // if (duration == 0) { // duration = EventWriter.timestamp() - startTime; // } // } methodVisitor.visitLabel(durationalEvent); methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, getInternalClassName(), FIELD_DURATION, "J"); methodVisitor.visitInsn(Opcodes.LCONST_0); methodVisitor.visitInsn(Opcodes.LCMP); methodVisitor.visitJumpInsn(Opcodes.IFNE, commit); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, TYPE_EVENT_HANDLER.getInternalName(), METHOD_TIME_STAMP.getName(), METHOD_TIME_STAMP.getDescriptor(), false); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, getInternalClassName(), FIELD_START_TIME, "J"); methodVisitor.visitInsn(Opcodes.LSUB); methodVisitor.visitFieldInsn(Opcodes.PUTFIELD, getInternalClassName(), FIELD_DURATION, "J"); methodVisitor.visitLabel(commit); // if (shouldCommit()) { methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, getInternalClassName(), METHOD_EVENT_SHOULD_COMMIT.getName(), METHOD_EVENT_SHOULD_COMMIT.getDescriptor(), false); Label end = new Label(); // eventHandler.write(...); // } methodVisitor.visitJumpInsn(Opcodes.IFEQ, end); getEventHandler(methodVisitor); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, eventHandlerXInternalName); for (FieldInfo fi : fieldInfos) { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, fi.internalClassName, fi.fieldName, fi.fieldDescriptor); } methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, eventHandlerXInternalName, writeMethod.getName(), writeMethod.getDescriptor(), false); methodVisitor.visitLabel(end); methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitEnd(); }); // MyEvent#shouldCommit() updateMethod(METHOD_EVENT_SHOULD_COMMIT, methodVisitor -> { Label fail = new Label(); if (guardHandlerReference) { getEventHandler(methodVisitor); methodVisitor.visitJumpInsn(Opcodes.IFNULL, fail); } // if (!eventHandler.shouldCommit(duration) goto fail; getEventHandler(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, getInternalClassName(), FIELD_DURATION, "J"); ASMToolkit.invokeVirtual(methodVisitor, TYPE_EVENT_HANDLER.getInternalName(), METHOD_EVENT_HANDLER_SHOULD_COMMIT); methodVisitor.visitJumpInsn(Opcodes.IFEQ, fail); for (SettingInfo si : settingInfos) { // if (!settingsMethod(eventHandler.settingX)) goto fail; methodVisitor.visitIntInsn(Opcodes.ALOAD, 0); if (untypedEventHandler) { methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, getInternalClassName(), FIELD_EVENT_HANDLER, TYPE_OBJECT.getDescriptor()); } else { methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, getInternalClassName(), FIELD_EVENT_HANDLER, Type.getDescriptor(EventHandler.class)); } methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, eventHandlerXInternalName); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, eventHandlerXInternalName, si.fieldName, TYPE_SETTING_CONTROL.getDescriptor()); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, si.internalSettingName); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, getInternalClassName(), si.methodName, "(" + si.settingDescriptor + ")Z", false); methodVisitor.visitJumpInsn(Opcodes.IFEQ, fail); } // return true methodVisitor.visitInsn(Opcodes.ICONST_1); methodVisitor.visitInsn(Opcodes.IRETURN); // return false methodVisitor.visitLabel(fail); methodVisitor.visitInsn(Opcodes.ICONST_0); methodVisitor.visitInsn(Opcodes.IRETURN); }); } private void getEventHandler(MethodVisitor methodVisitor) { if (untypedEventHandler) { methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, getInternalClassName(), FIELD_EVENT_HANDLER, TYPE_OBJECT.getDescriptor()); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, TYPE_EVENT_HANDLER.getInternalName()); } else { methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, getInternalClassName(), FIELD_EVENT_HANDLER, Type.getDescriptor(EventHandler.class)); } } private void makeUninstrumented() { updateExistingWithReturnFalse(METHOD_EVENT_SHOULD_COMMIT); updateExistingWithReturnFalse(METHOD_IS_ENABLED); updateExistingWithEmptyVoidMethod(METHOD_COMMIT); updateExistingWithEmptyVoidMethod(METHOD_BEGIN); updateExistingWithEmptyVoidMethod(METHOD_END); } private final void updateExistingWithEmptyVoidMethod(Method voidMethod) { updateMethod(voidMethod, methodVisitor -> { methodVisitor.visitInsn(Opcodes.RETURN); }); } private final void updateExistingWithReturnFalse(Method voidMethod) { updateMethod(voidMethod, methodVisitor -> { methodVisitor.visitInsn(Opcodes.ICONST_0); methodVisitor.visitInsn(Opcodes.IRETURN); }); } private MethodNode getMethodNode(Method method) { for (MethodNode m : classNode.methods) { if (m.name.equals(method.getName()) && m.desc.equals(method.getDescriptor())) { return m; } } return null; } private final void updateMethod(Method method, Consumer<MethodVisitor> code) { MethodNode old = getMethodNode(method); int index = classNode.methods.indexOf(old); classNode.methods.remove(old); MethodVisitor mv = classNode.visitMethod(old.access, old.name, old.desc, null, null); mv.visitCode(); code.accept(mv); mv.visitMaxs(0, 0); MethodNode newMethod = getMethodNode(method); classNode.methods.remove(newMethod); classNode.methods.add(index, newMethod); } public static Method makeWriteMethod(List<FieldInfo> fields) { StringBuilder sb = new StringBuilder(); sb.append("("); for (FieldInfo v : fields) { sb.append(v.fieldDescriptor); } sb.append(")V"); return new Method("write", sb.toString()); } private String getInternalClassName() { return classNode.name; } public List<SettingInfo> getSettingInfos() { return settingInfos; } public List<FieldInfo> getFieldInfos() { return fieldInfos; } public String getEventName() { return eventName; } public void setGuardHandler(boolean guardHandlerReference) { this.guardHandlerReference = guardHandlerReference; } }