/* * JCarder -- cards Java programs to keep threads disentangled * * Copyright (C) 2006-2007 Enea AB * Copyright (C) 2007 Ulrik Svensson * Copyright (C) 2007 Joel Rosdahl * * This program is made available under the GNU GPL version 2, with a special * exception for linking with JUnit. See the accompanying file LICENSE.txt for * details. * * This program 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. */ package com.enea.jcarder.analyzer; import static com.enea.jcarder.common.contexts.ContextFileReader.CONTEXTS_DB_FILENAME; import static com.enea.jcarder.common.contexts.ContextFileReader.EVENT_DB_FILENAME; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import com.enea.jcarder.common.LockingContext; import com.enea.jcarder.common.contexts.ContextFileReader; import com.enea.jcarder.common.contexts.ContextReaderIfc; import com.enea.jcarder.common.events.EventFileReader; import com.enea.jcarder.util.BuildInformation; import com.enea.jcarder.util.InvalidOptionException; import com.enea.jcarder.util.OptionParser; import com.enea.jcarder.util.logging.AppendableHandler; import com.enea.jcarder.util.logging.Handler; import com.enea.jcarder.util.logging.Logger; import com.enea.jcarder.util.logging.Logger.Level; /** * The main class of the JCarder analyzer. */ public final class Analyzer { enum OutputMode { INCLUDE_ALL, INCLUDE_CYCLES, INCLUDE_ONLY_MULTI_THREADED_CYCLES }; /* * Cycles with only one thread can never cause a deadlock, but it might be * possible that basic tests of a single class are very simplified and use * only a single thread where a real program might invoke the methods from * several different threads. Therefore single-threaded cycles are also * interesting to detect and include by default. */ private OutputMode mOutputMode = OutputMode.INCLUDE_CYCLES; private boolean mIncludePackages = false; private boolean mPrintDetails = false; private Logger mLogger; final private Level mLogLevel = Logger.Level.INFO; private String mInputDirectory = "."; public static void main(String[] args) { new Analyzer().start(args); } public void start(String[] args) { parseArguments(args); initLogger(); LockGraphBuilder graphBuilder = new LockGraphBuilder(); final ContextReaderIfc contextReader; try { contextReader = new ContextFileReader(mLogger, new File(mInputDirectory, CONTEXTS_DB_FILENAME)); EventFileReader eventReader = new EventFileReader(mLogger); eventReader.parseFile(new File(mInputDirectory, EVENT_DB_FILENAME), graphBuilder); } catch (IOException e) { mLogger.severe("Error while reading result database: " + e.getMessage()); return; } printInitiallyLoadedStatistics(graphBuilder.getAllLocks()); CycleDetector cycleDetector = new CycleDetector(mLogger); cycleDetector.analyzeLockNodes(graphBuilder.getAllLocks()); printCycleAnalysisStatistics(cycleDetector); if (mOutputMode == OutputMode.INCLUDE_ALL) { printDetailsIfEnabled(cycleDetector.getCycles(), contextReader); try { generatGraphvizFileForAllNodes(graphBuilder, contextReader); } catch (IOException e) { mLogger.severe("Error while generating Graphviz file: " + e.getMessage()); } } else { if (mOutputMode == OutputMode.INCLUDE_ONLY_MULTI_THREADED_CYCLES) { cycleDetector.removeSingleThreadedCycles(); } if (cycleDetector.getCycles().isEmpty()) { System.out.println("No cycles found!"); return; } graphBuilder.clear(); // Help GC. /* * TODO Also clear all references in LockNode.mOutgoingEdges to * avoid keeping references to a lot of LockEdge and LockNode * objects in order to release as much memory as possible for the * memory mapped file? * * It is not necessary to use the DuplicateEdgeshandler since those * duplicates are removed anyway when cycles that are alike are * removed. */ cycleDetector.removeAlikeCycles(contextReader); printDetailsIfEnabled(cycleDetector.getCycles(), contextReader); try { generateGraphvizFilesForCycles(contextReader, cycleDetector); } catch (IOException e) { mLogger.severe("Error while generating Graphviz file: " + e.getMessage()); } } } private void initLogger() { Collection<Handler> handlers = new ArrayList<Handler>(); handlers.add(new AppendableHandler(System.out, Logger.Level.CONFIG, "{message}\n")); mLogger = new Logger(handlers, mLogLevel); } private void generateGraphvizFilesForCycles(ContextReaderIfc reader, CycleDetector cycleDetector) throws IOException { System.out.println(); int index = 0; Collection<HashSet<LockEdge>> cycles = cycleDetector.mergeCyclesWithIdenticalLocks(); for (HashSet<LockEdge> edges : cycles) { if (index >= 100) { System.out.println("Aborting. Too many cycles!"); break; } GraphvizGenerator graphvizGenerator = new GraphvizGenerator(); createGraphvizFile(graphvizGenerator.generate(edges, reader, mIncludePackages), index++); } } private void printCycleAnalysisStatistics(CycleDetector cycleDetector) { System.out.println("\nCycle analysis result: "); System.out.println(" Cycles: " + cycleDetector.getCycles().size()); System.out.println(" Edges in cycles: " + cycleDetector.getNumberOfEdges()); System.out.println(" Nodes in cycles: " + cycleDetector.getNumberOfNodes()); System.out.println(" Max cycle depth: " + cycleDetector.getMaxCycleDepth()); System.out.println(" Max graph depth: " + cycleDetector.getMaxDepth()); System.out.println(); } private void generatGraphvizFileForAllNodes(LockGraphBuilder graphBuilder, ContextReaderIfc reader) throws IOException { DuplicatedEdgesHandler.mergeDuplicatedEdges(graphBuilder.getAllLocks(), reader); // TODO Print statistics about removed duplicates? LinkedList<LockEdge> allEdges = new LinkedList<LockEdge>(); for (LockNode node : graphBuilder.getAllLocks()) { allEdges.addAll(node.getOutgoingEdges()); } GraphvizGenerator graphvizGenerator = new GraphvizGenerator(); createGraphvizFile(graphvizGenerator.generate(allEdges, reader, mIncludePackages), 0); } private void parseArguments(String[] args) { OptionParser op = new OptionParser(); configureOptionParser(op); try { op.parse(args); } catch (InvalidOptionException e) { handleBadOption(op, e.getMessage()); } handleOptions(op); } private void configureOptionParser(OptionParser op) { /* * TODO Add parameters for filtering (including & excluding) specific * locks and edges for example by specifying thread names, object * classes, method names or packages? */ op.addOption("-help", "Print this help text"); op.addOption("-d <directory>", "Read results to analyze from <directory> (default:" + " current directory)"); op.addOption("-includepackages", "Include packages (not only class names) in graph"); op.addOption("-outputmode <mode>", "Set output mode to <mode> (one of ALL, CYCLES, MTCYCLES);" + " ALL: include everything;" + " CYCLES: only include cycles (this is the default);" + " MTCYCLES: only include multi-thread cycles"); op.addOption("-printdetails", "Print details"); op.addOption("-version", "Print program version"); } private void handleOptions(OptionParser op) { Map<String, String> options = op.getOptions(); for (String option : options.keySet()) { if (option.equals("-help")) { printHelpText(System.out, op); System.exit(0); } else if (option.equals("-i")) { mInputDirectory = options.get(option); } else if (option.equals("-includepackages")) { mIncludePackages = true; } else if (option.equals("-outputmode")) { String value = options.get(option); if (value.equalsIgnoreCase("all")) { mOutputMode = OutputMode.INCLUDE_ALL; } else if (value.equalsIgnoreCase("cycles")) { mOutputMode = OutputMode.INCLUDE_CYCLES; } else if (value.equalsIgnoreCase("mtcycles")) { mOutputMode = OutputMode.INCLUDE_ONLY_MULTI_THREADED_CYCLES; } else { handleBadOption(op, "bad output mode"); } } else if (option.equals("-printdetails")) { mPrintDetails = true; } else if (option.equals("-version")) { BuildInformation.printLongBuildInformation(); System.exit(0); } } } private void printHelpText(PrintStream stream, OptionParser op) { stream.print("Usage: java -jar jcarder.jar [options]\n\n"); stream.print("Options:\n"); stream.print(op.getOptionHelp()); } private void handleBadOption(OptionParser optionParser, String message) { System.err.println("JCarder: " + message); printHelpText(System.err, optionParser); System.exit(1); } private void printDetailsIfEnabled(Iterable<Cycle> cycles, ContextReaderIfc reader) { if (!mPrintDetails) { return; } SortedSet<String> threads = new TreeSet<String>(); SortedSet<String> methods = new TreeSet<String>(); for (Cycle cycle : cycles) { for (LockEdge edge : cycle.getEdges()) { LockingContext source = reader.readContext(edge.getSourceLockingContextId()); LockingContext target = reader.readContext(edge.getTargetLockingContextId()); threads.add(source.getThreadName()); threads.add(target.getThreadName()); methods.add(source.getMethodWithClass()); methods.add(target.getMethodWithClass()); } } System.out.println(); System.out.println("Threads involved in cycles:"); for (String thread : threads) { System.out.println(" " + thread); } System.out.println(); System.out.println("Methods involved in cycles:"); for (String method : methods) { System.out.println(" " + method); } System.out.println(); } private void printInitiallyLoadedStatistics(Iterable<LockNode> locks) { int numberOfNodes = 0; int numberOfUniqueEdges = 0; int numberOfDuplicatedEdges = 0; for (LockNode lock : locks) { numberOfNodes++; numberOfUniqueEdges += lock.numberOfUniqueEdges(); numberOfDuplicatedEdges += lock.numberOfDuplicatedEdges(); } System.out.println("\nLoaded from database files:"); System.out.println(" Nodes: " + numberOfNodes); System.out.println(" Edges: " + numberOfUniqueEdges + " (excluding " + numberOfDuplicatedEdges + " duplicated)"); } private void createGraphvizFile(String s, int index) throws IOException { File file = new File("jcarder_result_" + index + ".dot"); System.out.println("Writing Graphviz file: " + file.getAbsolutePath()); FileWriter fw = new FileWriter(file); fw.write(s); fw.flush(); fw.close(); } }