/* * FindBugs - Find Bugs in Java programs * Copyright (C) 2003-2007 University of Maryland * * This library 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 library 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 library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package edu.umd.cs.findbugs.log; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; import java.util.Comparator; import java.util.EmptyStackException; import java.util.Stack; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import edu.umd.cs.findbugs.FindBugs2; import edu.umd.cs.findbugs.SystemProperties; import edu.umd.cs.findbugs.ba.AnalysisContext; import edu.umd.cs.findbugs.xml.XMLOutput; import edu.umd.cs.findbugs.xml.XMLWriteable; /** * @author pugh */ public class Profiler implements XMLWriteable { final static boolean REPORT = SystemProperties.getBoolean("profiler.report"); final static boolean MAX_CONTEXT = SystemProperties.getBoolean("findbugs.profiler.maxcontext"); public Profiler() { startTimes = new Stack<Clock>(); profile = new ConcurrentHashMap<Class<?>, Profile>(); if (REPORT) System.err.println("Profiling activated"); } public static interface Filter { public boolean accepts(Profile p); } public static class FilterByTime implements Filter { private final long minNanoSeconds; public FilterByTime(long minNanoSeconds) { this.minNanoSeconds = minNanoSeconds; } public boolean accepts(Profile p) { long time = p.totalTime.get(); if (time < minNanoSeconds) { return false; } return true; } } public static class FilterByTimePerCall implements Filter { private final long minNanoSeconds; public FilterByTimePerCall(long minNanoSeconds) { this.minNanoSeconds = minNanoSeconds; } public boolean accepts(Profile p) { int totalCalls = p.totalCalls.get(); long time = p.totalTime.get(); if (time / totalCalls < minNanoSeconds) { return false; } return true; } } public static class FilterByCalls implements Filter { private final int minCalls; public FilterByCalls(int minCalls) { this.minCalls = minCalls; } public boolean accepts(Profile p) { int totalCalls = p.totalCalls.get(); if (totalCalls < minCalls) { return false; } return true; } } public static class Profile implements XMLWriteable { /** time in nanoseconds */ final AtomicLong totalTime = new AtomicLong(); final AtomicInteger totalCalls = new AtomicInteger(); /** time in nanoseconds */ final AtomicLong maxTime = new AtomicLong(); final AtomicLong totalSquareMicroseconds = new AtomicLong(); private final String className; Object maxContext; /** * @param className * non null full qualified class name */ public Profile(String className) { this.className = className; } public void handleCall(long nanoTime, Object context) { totalCalls.incrementAndGet(); totalTime.addAndGet(nanoTime); long oldMax = maxTime.get(); if (nanoTime > oldMax) { maxTime.compareAndSet(oldMax, nanoTime); if (MAX_CONTEXT) maxContext = context; } long microseconds = TimeUnit.MICROSECONDS.convert(nanoTime, TimeUnit.NANOSECONDS); totalSquareMicroseconds.addAndGet(microseconds * microseconds); } public long getTotalTime() { return totalTime.get(); } /** * @param xmlOutput * @throws IOException */ public void writeXML(XMLOutput xmlOutput) throws IOException { long time = totalTime.get(); int callCount = totalCalls.get(); long maxTimeMicros = TimeUnit.MICROSECONDS.convert(maxTime.get(), TimeUnit.NANOSECONDS); long timeMillis = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS); long timeMicros = TimeUnit.MICROSECONDS.convert(time, TimeUnit.NANOSECONDS); long averageTimeMicros = timeMicros / callCount; long totalSquareMicros = totalSquareMicroseconds.get(); long averageSquareMicros = totalSquareMicros / callCount; long timeVariance = averageSquareMicros - averageTimeMicros * averageTimeMicros; long timeStandardDeviation = (long) Math.sqrt(timeVariance); if (timeMillis > 10) { xmlOutput.startTag("ClassProfile"); xmlOutput.addAttribute("name", className); xmlOutput.addAttribute("totalMilliseconds", String.valueOf(timeMillis)); xmlOutput.addAttribute("invocations", String.valueOf(callCount)); xmlOutput.addAttribute("avgMicrosecondsPerInvocation", String.valueOf(averageTimeMicros)); xmlOutput.addAttribute("maxMicrosecondsPerInvocation", String.valueOf(maxTimeMicros)); if (maxContext != null) xmlOutput.addAttribute("maxContext", String.valueOf(maxContext)); xmlOutput.addAttribute("standardDeviationMircosecondsPerInvocation", String.valueOf(timeStandardDeviation)); xmlOutput.stopTag(true); } } } static class Clock { final Class<?> clazz; long startTimeNanos; long accumulatedTime; Clock(Class<?> clazz, long currentNanoTime) { this.clazz = clazz; startTimeNanos = currentNanoTime; } void accumulateTime(long currentNanoTime) { accumulatedTime += currentNanoTime - startTimeNanos; } void restartClock(long currentNanoTime) { startTimeNanos = currentNanoTime; } } final Stack<Clock> startTimes; final ConcurrentHashMap<Class<?>, Profile> profile; final Stack<Object> context = new Stack<Object>(); public void startContext(Object context) { this.context.push(context); } public void endContext(Object context) { Object o = this.context.pop(); assert o == context; } private Object getContext() { if (context.size() == 0) return ""; try { return context.peek(); } catch (EmptyStackException e) { return ""; } } public void start(Class<?> c) { long currentNanoTime = System.nanoTime(); Stack<Clock> stack = startTimes; if (!stack.isEmpty()) { stack.peek().accumulateTime(currentNanoTime); } stack.push(new Clock(c, currentNanoTime)); // System.err.println("push " + c.getSimpleName()); } public void end(Class<?> c) { // System.err.println("pop " + c.getSimpleName()); long currentNanoTime = System.nanoTime(); Stack<Clock> stack = startTimes; Clock ending = stack.pop(); if (ending.clazz != c) { throw new AssertionError("Asked to end timing for " + c + " but top of stack is " + ending.clazz + ", remaining stack is " + stack); } ending.accumulateTime(currentNanoTime); if (!stack.isEmpty()) { Clock restarting = stack.peek(); restarting.restartClock(currentNanoTime); } long accumulatedTime = ending.accumulatedTime; if (accumulatedTime == 0) { return; } Profile counter = profile.get(c); if (counter == null) { counter = new Profile(c.getName()); Profile counter2 = profile.putIfAbsent(c, counter); if (counter2 != null) { counter = counter2; } } counter.handleCall(accumulatedTime, getContext()); } public static class ClassNameComparator implements Comparator<Class<?>>, Serializable { final protected Profiler profiler; public ClassNameComparator(Profiler p) { this.profiler = p; } public int compare(Class<?> c1, Class<?> c2) { try { return c1.getSimpleName().compareTo(c2.getSimpleName()); } catch (RuntimeException e) { AnalysisContext.logError("Error comparing " + c1 + " and " + c2, e); int i1 = System.identityHashCode(c1); int i2 = System.identityHashCode(c2); if (i1 < i2) return -1; if (i1 > i2) return 1; return 0; } } } public static class TotalTimeComparator extends ClassNameComparator { public TotalTimeComparator(Profiler p) { super(p); } @Override public int compare(Class<?> c1, Class<?> c2) { long v1 = profiler.getProfile(c1).totalTime.get(); long v2 = profiler.getProfile(c2).totalTime.get(); if (v1 < v2) { return 1; } if (v1 > v2) { return -1; } return super.compare(c1, c2); } } public static class TimePerCallComparator extends ClassNameComparator { public TimePerCallComparator(Profiler p) { super(p); } @Override public int compare(Class<?> c1, Class<?> c2) { Profile profile1 = profiler.getProfile(c1); Profile profile2 = profiler.getProfile(c2); long time1 = profile1.totalTime.get() / profile1.totalCalls.get(); long time2 = profile2.totalTime.get() / profile2.totalCalls.get(); if (time1 < time2) { return 1; } if (time1 > time2) { return -1; } return super.compare(c1, c2); } } public static class TotalCallsComparator extends ClassNameComparator { public TotalCallsComparator(Profiler p) { super(p); } @Override public int compare(Class<?> c1, Class<?> c2) { Profile profile1 = profiler.getProfile(c1); Profile profile2 = profiler.getProfile(c2); int calls1 = profile1.totalCalls.get(); int calls2 = profile2.totalCalls.get(); if (calls1 < calls2) { return 1; } if (calls1 > calls2) { return -1; } return super.compare(c1, c2); } } /** * Default implementation uses {@link TotalTimeComparator} and prints out * class statistics based on total time spent fot a class */ public void report() { if (!REPORT) { return; } report(new TotalTimeComparator(this), new FilterByTime(10000000), System.err); } /** * @param reportComparator * non null comparator instance which will be used to sort the * report statistics */ public void report(Comparator<Class<?>> reportComparator, Filter filter, PrintStream stream) { stream.println("PROFILE REPORT"); try { TreeSet<Class<?>> treeSet = new TreeSet<Class<?>>(reportComparator); treeSet.addAll(profile.keySet()); stream.printf("%8s %8s %9s %s%n", "msecs", "#calls", "usecs/call", "Class"); for (Class<?> c : treeSet) { Profile p = getProfile(c); long time = p.totalTime.get(); int callCount = p.totalCalls.get(); if (filter.accepts(p)) { stream.printf("%8d %8d %8d %s%n", Long.valueOf(TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS)), Integer.valueOf(callCount), Long.valueOf(TimeUnit.MICROSECONDS.convert(time / callCount, TimeUnit.NANOSECONDS)), c.getSimpleName()); } } stream.flush(); } catch (RuntimeException e) { System.err.println(e); } } /** * Clears the previously accumulated data. This method is public because it * can be accessed explicitely from clients (like Eclipse). * <p> * There is no need to clear profiler data after each run, because a new * profiler instance is used for each analysis run (see * {@link FindBugs2#execute()}). */ public void clear() { profile.clear(); startTimes.clear(); } public Profile getProfile(Class<?> c) { Profile result = profile.get(c); if (result == null) { AnalysisContext.logError("Unexpected null profile for " + c.getName(), new NullPointerException()); result = new Profile(c.getName()); Profile tmp = profile.putIfAbsent(c, result); if (tmp != null) return tmp; } return result; } /* * (non-Javadoc) * * @see * edu.umd.cs.findbugs.xml.XMLWriteable#writeXML(edu.umd.cs.findbugs.xml * .XMLOutput) */ public void writeXML(XMLOutput xmlOutput) throws IOException { xmlOutput.startTag("FindBugsProfile"); xmlOutput.stopTag(false); TreeSet<Class<?>> treeSet = new TreeSet<Class<?>>(new TotalTimeComparator(this)); treeSet.addAll(profile.keySet()); long totalTime = 0; for (Profile p : profile.values()) totalTime += p.totalTime.get(); long accumulatedTime = 0; for (Class<?> c : treeSet) { Profile p = getProfile(c); if (p == null) continue; p.writeXML(xmlOutput); accumulatedTime += p.totalTime.get(); if (accumulatedTime > 3 * totalTime / 4) break; } xmlOutput.closeTag("FindBugsProfile"); } }