/* * 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 java.io.PrintStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * Class used to collect details of methods which need to be updated by the agent. */ public class UpdateSet { /** * value used as a marker to identify entries in the concurrent hash map index */ private static Object PRESENT = new Object(); private final AtomicInteger renotifications = new AtomicInteger(); private final AtomicInteger indexRaces = new AtomicInteger(); private final AtomicInteger insertionRaces = new AtomicInteger(); /** * index by classname and methodname used to detect entries which have already been * notified. note we use a normal hashmap and synchronize explicitly on it because * we want the inserting clients threads and the removing agent thread to be able * to sleep on this index. */ private HashMap<String, Object> classMethodIndex; /** * a map keyed by class#method to record when a specific method was first notified */ private ConcurrentHashMap<String, Long> notifiedTimestamps; /** * a map keyed by class#method to record when a notify for a specific method was noticed by the agent */ private ConcurrentHashMap<String, Long> processedTimestamps; /** * a map keyed by class#method to record when a the agent first performed transformation of a specific method */ private ConcurrentHashMap<String, List<Long>> transformedTimestamps; /** * index by classname which allows identification of methods associated with */ private ConcurrentHashMap<String, MethodUpdateSet> classIndex; public UpdateSet() { this(false); } public UpdateSet(boolean trackTransforms) { classMethodIndex = new HashMap<String, Object>(); classIndex = new ConcurrentHashMap<String, MethodUpdateSet>(); // we timestamp class#method entries when they are notified, detected by the agent and trasformed // the notified timestamps are set when an entry is added to the agent's notified update set // the processed timestamps are set when an entry is added to the agent's transform update set // and the notified timestamp is copied across from notified to transform update set at the same time // the transformed timestamp is updated when the transformed bytecode is generated notifiedTimestamps = new ConcurrentHashMap<String, Long>(); if (trackTransforms) { processedTimestamps = new ConcurrentHashMap<String, Long>(); transformedTimestamps = new ConcurrentHashMap<String, List<Long>>(); } else { processedTimestamps = null; transformedTimestamps = null; } } /** * add an entry to the update set if it is not already present * @param className the name of the class to be updated * @param methodName the name of the method of thast class to be updated * @return true if the entry has been added or false if it is already present */ public boolean add(String className, String methodName) { // the classmethod index provides a quick check allowing us to avoid // synchronizing on the full index if we have already seen this entry // this avoids the situation where the more extensive processing needed // to handle new index entries locks out threads which are merely // notifying a known entry. String classMethodName = className + "#" + methodName; boolean present; synchronized (classMethodIndex) { present = classMethodIndex.put(classMethodName, PRESENT) == null; } if (!present) { // track renotifications for performance checking renotifications.incrementAndGet(); return false; } else if (processedTimestamps != null) { processedTimestamps.put(classMethodName, System.currentTimeMillis()); } else { notifiedTimestamps.put(classMethodName, System.currentTimeMillis()); } // this is a new entry so update the full index return fullyIndex(className, methodName); } /** * insert a newly notified entry into a method update set which is indexed in the class * index by the owner class name * @param className * @param methodName * @return true if the entry was successfully added and false if it was already present (not * sure yet that this should ever return false) */ private boolean fullyIndex(String className, String methodName) { MethodUpdateSet methodUpdates = classIndex.get(className); if (methodUpdates == null) { // use putIfAbsent to resolve insertion races for the method update set MethodUpdateSet newMethodUpdates = new MethodUpdateSet(className); methodUpdates = classIndex.putIfAbsent(className, newMethodUpdates); // count if we had an indexing race if (methodUpdates != null) { indexRaces.incrementAndGet(); } else { methodUpdates = newMethodUpdates; } } // we may see a repeat entry because we started then insert just as an old entry was being // transferred if (!methodUpdates.add(methodName)) { insertionRaces.incrementAndGet(); return false; } return true; } /** * transfer entries from this set into a target set where they are not present and also record * newly added entries in a difference set. the target set may not be concurrently modified while * this operation is in progress but this set may be modified concurrently. * @param target the set to which entries from this set may be added * @return the difference set containing all entries added to the target set */ public UpdateSet transfer(UpdateSet target) { UpdateSet diff = new UpdateSet(); // iterate over all entries in the table Enumeration<MethodUpdateSet> methodUpdateSets = classIndex.elements(); while(methodUpdateSets.hasMoreElements()) { // retrieve the class and method names for this entry, resetting the method name set to empty MethodUpdateSet methodUpdateSet = methodUpdateSets.nextElement(); String className = methodUpdateSet.getClassName(); List<String> methodNames = methodUpdateSet.reset(); if (methodNames != null) { // copy the entries to the target set and, where appropriate, the difference set for (String methodName : methodNames) { // first remove the entry so that we can sleep when the index is empty. // we will eventually stop being renotified because the bytecode transform // will bypass each call to the notifying method // TODO hmm, that last statement is maybe not certain e.g. if for some reason we cannot // transform a specific class. if that happens then we may need to do the delete from // a shadow index and retain the main index list to avoid renotifications String classMethodName = className + "#" + methodName; synchronized (classMethodIndex) { classMethodIndex.remove(classMethodName); } if (target.add(className, methodName)) { // propagate the notified timestamp Long notifiedTimestamp = notifiedTimestamps.get(classMethodName); target.notifiedTimestamps.put(classMethodName, notifiedTimestamp); // add tis to the diff set so we retransform the class diff.add(className, methodName); } } } } // the difference set now contains entries for all classes which we need to retransform return diff; } public List<String> classNames() { if (classMethodIndex.isEmpty()) { return null; } List<String> classNames = new ArrayList<String>(); Enumeration<String> keys = classIndex.keys(); while (keys.hasMoreElements()) { classNames.add(keys.nextElement()); } return classNames; } // TODO -- these two methods probably ought to be wrapped up internally rather than being exposed to clients /** * this is called after a successful notification of a new entry to ensure that the Jokre agent wakes up and * processes the entry. note that the lock is not taken until notification which means we may get false * wakeups but that means inserting threads don't see a big delay which is what we want. */ public void wakeup() { synchronized (classMethodIndex) { classMethodIndex.notifyAll(); } } /** * this is called by the Jokre agent thread after it has finished transferring notifications from the staging * update set to the installed update set. it causes the Jokre agent to wait until new updates are available. */ public void waitForUpdates() { synchronized (classMethodIndex) { while (classMethodIndex.isEmpty()) { try { classMethodIndex.wait(); } catch (InterruptedException ie) { // ignore } } } } public List<String> listMethods(String className) { MethodUpdateSet updateSet = classIndex.get(className); if (updateSet != null) { return updateSet.collect(); } else { return null; } } public void transformed(String className, List<String> methodNames) { // no need for null check as this is only ever called on the update set for (String methodname : methodNames) { String classMethodName = className + "#" + methodname; // no need for lock as this only ever happens single-threaded List<Long> timestamps = transformedTimestamps.get(classMethodName); if (timestamps == null) { timestamps = new ArrayList<Long>(); transformedTimestamps.put(classMethodName, timestamps); } synchronized (timestamps) { timestamps.add(System.currentTimeMillis()); } } } public void stats() { Set<String> classMethodKeys = classMethodIndex.keySet(); Set<String> classKeys = classIndex.keySet(); System.out.println("class count: " + classKeys.size()); dumpNames(System.out, classKeys); System.out.println("entry count: " + classMethodKeys.size()); if (transformedTimestamps == null) { dumpNames(System.out, classMethodKeys); } else { dumpTimestamps(System.out, classMethodKeys); } System.out.println("renotifications: " + renotifications); System.out.println("indexRaces: " + indexRaces); System.out.println("insertionRaces: " + insertionRaces); } private void dumpNames(PrintStream out, Set<String> classes) { StringBuilder builder = new StringBuilder(); Iterator<String> iterator = classes.iterator(); builder.append("["); String prefix = ""; while (iterator.hasNext()) { String next = iterator.next(); builder.append(prefix); builder.append(next); prefix = ", "; } builder.append("]"); out.println(builder.toString()); } private void dumpTimestamps(PrintStream out, Set<String> classMethodNames) { StringBuilder builder = new StringBuilder(); Iterator<String> iterator = classMethodNames.iterator(); builder.append(" ["); String prefix = "\n "; while (iterator.hasNext()) { String classMethodName = iterator.next(); Long notified = notifiedTimestamps.get(classMethodName); Long processed = processedTimestamps.get(classMethodName); List<Long> transformed = transformedTimestamps.get(classMethodName); builder.append(prefix); builder.append(classMethodName); // ensure we did not catch this in mid update if (notified == null) { builder.append("transferring . . ."); } else if (processed == null) { builder.append(" notify "); builder.append(System.currentTimeMillis() - notified); builder.append( "ms"); } else if (transformed == null) { builder.append(" notify "); builder.append(processed - notified); builder.append( "ms process ") ; builder.append(System.currentTimeMillis() - processed); builder.append( "ms"); } else { builder.append(" notified "); builder.append(processed - notified); builder.append( "ms process") ; synchronized(transformed) { for (int i = 0; i < transformed.size(); i++) { builder.append( " "); builder.append(transformed.get(i) - processed); builder.append( "ms transform"); } } } } builder.append("\n ]"); out.println(builder.toString()); } public class MethodUpdateSet { public MethodUpdateSet(String className) { this.className = className; this.methods = new ConcurrentHashMap<String, Object>(); } public boolean add(String methodName) { return (methods.putIfAbsent(methodName, PRESENT) == null); } public String getClassName() { return className; } public List<String> reset() { Enumeration<String> enumeration = methods.keys(); if (enumeration.hasMoreElements()) { ArrayList<String> methodNames = new ArrayList<String>(); while (enumeration.hasMoreElements()) { String methodName = enumeration.nextElement(); methodNames.add(methodName); methods.remove(methodName); } return methodNames; } else { return null; } } public List<String> collect() { Enumeration<String> enumeration = methods.keys(); if (enumeration.hasMoreElements()) { ArrayList<String> methodNames = new ArrayList<String>(); while (enumeration.hasMoreElements()) { String methodName = enumeration.nextElement(); methodNames.add(methodName); } return methodNames; } else { return null; } } private String className; private ConcurrentHashMap<String, Object> methods; } }