/* * JBoss, Home of Professional Open Source * Copyright 2011, Red Hat and individual contributors * by the @authors tag. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * * @authors Andrew Dinn */ package org.jboss.jokre.agent; import org.jboss.jokre.transformer.JokreTransformer; import org.jboss.jokre.transformer.MapAdapterConstants; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.List; /** * Jokre agent transformer used to retransform call sites for Map.put calls so that they call an optimized method instead */ public class Jokre implements ClassFileTransformer { // public API /** * validate that the caller is a put method then add the classname and method name of its caller * to the Jokre agent's update set * @return true if the caller has not yet been added to the update set or false if it has been added */ public static boolean notifyMapPut() { Thread currentThread = Thread.currentThread(); StackTraceElement[] stackTrace = currentThread.getStackTrace(); // we should get called from a Map.put implementation and we want to replace its caller int length = stackTrace.length; if (length < 4) { throw new InvalidNotifyException("notifyMapPut must be called below Map.put call site"); } StackTraceElement putCall = stackTrace[2]; if (putCall.getMethodName() != "put") { throw new InvalidNotifyException("notifyMapPut must be called from Map.put implementation"); } StackTraceElement putCaller = stackTrace[3]; if (putCaller.isNativeMethod()) { System.err.println("oops put called via native code!!!"); // hmm, this would be a call site we cannot modify. this should never happen! return false; } String callerClass = putCaller.getClassName(); String callerMethod = putCaller.getMethodName(); if (theJokre == null) { System.err.println("Jokre java agent is not installed!!!"); } return theJokre.addToStaging(callerClass, callerMethod); } /** * dump statistics detailing notifications and renotifications into the staging set * and the update set */ public static void stats() { System.out.println("Staging"); theJokre.staging.stats(); System.out.println("Updates"); theJokre.updated.stats(); } // public constructor for use by Jokre Main class // n.b. the Jokre Main class is loaded via the system classpath like any java agent // it installs the jar containing this class into the bootstrap classpath then constructs // an instance reflectively, thus ensuring it uses the correct version of this class. public Jokre(Instrumentation inst) { synchronized (Jokre.class) { if (theJokre != null) { throw new RuntimeException("Invalid attempt to create Jokre agent"); } theJokre = this; } this.inst = inst; checkInfinispan(); jokreTransformer = new JokreTransformer(); jokreThread = new JokreThread(this); jokreThread.start(); } // protected API for use by background thread protected void runJokre() { while (true) { waitForUpdates(); UpdateSet diffs = staging.transfer(updated); List<String> classNames = diffs.classNames(); if (classNames != null) { retransform(classNames); } } } // private implementation /** * Instrumentation object providing access to the JDK class base */ private Instrumentation inst; /** * singleton Jokre instance which manages all updates */ private static Jokre theJokre = null; /** * a background thread used to process agent notifications */ private JokreThread jokreThread = null; /** * a transformer which modifies callers which invoke modified put calls */ private JokreTransformer jokreTransformer = null; /** * the agent uses a staging update set to record updates which have not yet been retransformed */ private UpdateSet staging = new UpdateSet(); /** * the agent uses a second update set to record updates which have been retransformed */ private UpdateSet updated = new UpdateSet(true); /** * ensure that no infinispan classes have been loaded into the runtime */ private void checkInfinispan() { for (Class clazz : inst.getAllLoadedClasses()) { if (isMapImplementorClass(clazz.getName())) { throw new RuntimeException("Invalid attempt to load Jokre agent after loading infinispan"); } } } private boolean addToStaging(String callerClass, String callerMethod) { boolean result = staging.add(callerClass, callerMethod); // m.b. the locking scheme may mean that we rewake the agent after it has just processed that // insert but that does no harm and and it ensures that the notify is fast because we don't hold // a lock held by the agent for long. for this to work the agent has to be sure the staging set // is empty before it sleeps and releases the wakeup lock. that ensures we don;t miss any wakeups. if (result) { wakeup(); } return result; } private void wakeup() { // wakeup any threads waiting for entries to be added to the staging updates set staging.wakeup(); } private void waitForUpdates() { // wait only when the staging updates set is empty staging.waitForUpdates(); } private void retransform(List<String> classNames) { Class[] classes = inst.getAllLoadedClasses(); for (Class clazz : classes) { String name = clazz.getName(); if (classNames.contains(name)) { try { inst.retransformClasses(clazz); } catch (Exception e) { // oops -- what is the consequence of this? // if we get an exception here then the client will keep on renotifying // a call to the slow path method which will slow down calls via this path // a little. this may not be significant and must be weighed against gains // made elsewhere. dumping stats on renotifications will show where this // is happening. } } } } // // background thread to process notifications from instrumented API // private static class JokreThread extends Thread { private Jokre jokreInstance; public JokreThread(Jokre theJokre) { this.jokreInstance = theJokre; this.setDaemon(true); this.setName("The Jokre"); } public void run() { jokreInstance.runJokre(); } } // // implementation of interface ClassFileTransformer // /** * transformer entry point * @param loader * @param className * @param classBeingRedefined * @param protectionDomain * @param classfileBuffer * @return * @throws IllegalClassFormatException */ public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { byte[] bytes = classfileBuffer; if (isMapImplementorClass(className)) { // modify this implementation so it supports a void put bytes = jokreTransformer.extendMapImplementorAPI(loader, className, classBeingRedefined, protectionDomain, bytes); } String classNameExternal = className.replace('/', '.'); List<String> methodNames = updated.listMethods(classNameExternal); if (methodNames != null) { bytes = jokreTransformer.transform(loader, className, classBeingRedefined, protectionDomain, bytes, methodNames); updated.transformed(classNameExternal, methodNames); } return bytes; } private boolean isMapImplementorClass(String className) { // if appropriate we can extend this test to include other top level // Map implementors such as AbstractDelegatingCache return (className.equals(MapAdapterConstants.CLASS_CACHE_SUPPORT) || className.equals(MapAdapterConstants.CLASS_ABSTRACT_DELEGATING_CACHE)); } }