/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2012 ForgeRock AS */ package org.opends.server.plugins.profiler; import org.opends.messages.Message; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Font; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTree; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import org.opends.server.protocols.asn1.*; import org.opends.server.util.args.ArgumentException; import org.opends.server.util.args.ArgumentParser; import org.opends.server.util.args.BooleanArgument; import org.opends.server.util.args.StringArgument; import static org.opends.messages.PluginMessages.*; import static org.opends.messages.ToolMessages.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines a Directory Server utility that may be used to view * profile information that has been captured by the profiler plugin. It * supports viewing this information in either a command-line mode or using a * simple GUI. */ public class ProfileViewer implements TreeSelectionListener { // The root stack frames for the profile information that has been captured. private HashMap<ProfileStackFrame,ProfileStackFrame> rootFrames; // A set of stack traces indexed by class and method name. private HashMap<String,HashMap<ProfileStack,Long>> stacksByMethod; // The editor pane that will provide detailed information about the selected // stack frame. private JEditorPane frameInfoPane; // The GUI tree that will be used to hold stack frame information; private JTree profileTree; // The total length of time in milliseconds for which data is available. private long totalDuration; // The total number of profile intervals for which data is available. private long totalIntervals; /** * Parses the command-line arguments and creates an instance of the profile * viewer as appropriate. * * @param args The command-line arguments provided to this program. */ public static void main(String[] args) { // Define the command-line arguments that may be used with this program. BooleanArgument displayUsage; BooleanArgument useGUI = null; StringArgument fileNames = null; // Create the command-line argument parser for use with this program. Message toolDescription = INFO_PROFILEVIEWER_TOOL_DESCRIPTION.get(); ArgumentParser argParser = new ArgumentParser("org.opends.server.plugins.profiler.ProfileViewer", toolDescription, false); // Initialize all the command-line argument types and register them with the // parser. try { fileNames = new StringArgument("filenames", 'f', "fileName", true, true, true, INFO_FILE_PLACEHOLDER.get(), null, null, INFO_PROFILEVIEWER_DESCRIPTION_FILENAMES.get()); argParser.addArgument(fileNames); useGUI = new BooleanArgument( "usegui", 'g', "useGUI", INFO_PROFILEVIEWER_DESCRIPTION_USE_GUI.get()); argParser.addArgument(useGUI); displayUsage = new BooleanArgument( "help", 'H', "help", INFO_PROFILEVIEWER_DESCRIPTION_USAGE.get()); argParser.addArgument(displayUsage); argParser.setUsageArgument(displayUsage); } catch (ArgumentException ae) { Message message = ERR_PROFILEVIEWER_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()); System.err.println(message); System.exit(1); } // Parse the command-line arguments provided to this program. try { argParser.parseArguments(args); } catch (ArgumentException ae) { Message message = ERR_PROFILEVIEWER_ERROR_PARSING_ARGS.get(ae.getMessage()); System.err.println(message); System.err.println(argParser.getUsage()); System.exit(1); } // If we should just display usage or versionn information, // then print it and exit. if (argParser.usageOrVersionDisplayed()) { System.exit(0); } // Create the profile viewer and read in the data files. ProfileViewer viewer = new ProfileViewer(); for (String filename : fileNames.getValues()) { try { viewer.processDataFile(filename); } catch (Exception e) { Message message = ERR_PROFILEVIEWER_CANNOT_PROCESS_DATA_FILE.get(filename, stackTraceToSingleLineString(e)); System.err.println(message); } } // Write the captured information to standard output or display it in a GUI. if (useGUI.isPresent()) { viewer.displayGUI(); } else { viewer.printProfileData(); } } /** * Creates a new profile viewer object without any data. It should be * populated with one or more calls to <CODE>processDataFile</CODE> */ public ProfileViewer() { rootFrames = new HashMap<ProfileStackFrame,ProfileStackFrame>(); stacksByMethod = new HashMap<String,HashMap<ProfileStack,Long>>(); totalDuration = 0; totalIntervals = 0; } /** * Reads and processes the information in the provided data file into this * profile viewer. * * @param filename The path to the file containing the data to be read. * * @throws IOException If a problem occurs while trying to read from the * data file. * * @throws ASN1Exception If an error occurs while trying to decode the * contents of the file into profile stack objects. */ public void processDataFile(String filename) throws IOException, ASN1Exception { // Try to open the file for reading. ASN1Reader reader = ASN1.getReader(new FileInputStream(filename)); try { // The first element in the file must be a sequence with the header // information. reader.readStartSequence(); totalIntervals += reader.readInteger(); long startTime = reader.readInteger(); long stopTime = reader.readInteger(); totalDuration += (stopTime - startTime); reader.readEndSequence(); // The remaining elements will contain the stack frames. while (reader.hasNextElement()) { ProfileStack stack = ProfileStack.decode(reader); long count = reader.readInteger(); int pos = stack.getNumFrames() - 1; if (pos < 0) { continue; } String[] classNames = stack.getClassNames(); String[] methodNames = stack.getMethodNames(); int[] lineNumbers = stack.getLineNumbers(); ProfileStackFrame frame = new ProfileStackFrame(classNames[pos], methodNames[pos]); ProfileStackFrame existingFrame = rootFrames.get(frame); if (existingFrame == null) { existingFrame = frame; } String classAndMethod = classNames[pos] + "." + methodNames[pos]; HashMap<ProfileStack,Long> stackMap = stacksByMethod.get(classAndMethod); if (stackMap == null) { stackMap = new HashMap<ProfileStack,Long>(); stacksByMethod.put(classAndMethod, stackMap); } stackMap.put(stack, count); existingFrame.updateLineNumberCount(lineNumbers[pos], count); rootFrames.put(existingFrame, existingFrame); existingFrame.recurseSubFrames(stack, pos-1, count, stacksByMethod); } } finally { try { reader.close(); } catch (Exception e) {} } } /** * Retrieves an array containing the root frames for the profile information. * The array will be sorted in descending order of matching stacks. The * elements of this array will be the leaf method names with sub-frames * holding information about the callers of those methods. * * @return An array containing the root frames for the profile information. */ public ProfileStackFrame[] getRootFrames() { ProfileStackFrame[] frames = new ProfileStackFrame[0]; frames = rootFrames.values().toArray(frames); Arrays.sort(frames); return frames; } /** * Retrieves the total number of sample intervals for which profile data is * available. * * @return The total number of sample intervals for which profile data is * available. */ public long getTotalIntervals() { return totalIntervals; } /** * Retrieves the total duration in milliseconds covered by the profile data. * * @return The total duration in milliseconds covered by the profile data. */ public long getTotalDuration() { return totalDuration; } /** * Prints the profile information to standard output in a human-readable * form. */ public void printProfileData() { System.out.println("Total Intervals: " + totalIntervals); System.out.println("Total Duration: " + totalDuration); System.out.println(); System.out.println(); for (ProfileStackFrame frame : getRootFrames()) { printFrame(frame, 0); } } /** * Prints the provided stack frame and its subordinates using the provided * indent. * * @param frame The stack frame to be printed, followed by recursive * information about all its subordinates. * @param indent The number of tabs to indent the stack frame information. */ private static void printFrame(ProfileStackFrame frame, int indent) { for (int i=0; i < indent; i++) { System.out.print("\t"); } System.out.print(frame.getTotalCount()); System.out.print("\t"); System.out.print(frame.getClassName()); System.out.print("."); System.out.println(frame.getMethodName()); if (frame.hasSubFrames()) { for (ProfileStackFrame f : frame.getSubordinateFrames()) { printFrame(f, indent+1); } } } /** * Displays a simple GUI with the profile data. */ public void displayGUI() { JFrame appWindow = new JFrame("Directory Server Profile Data"); appWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); Container contentPane = appWindow.getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.setFont(new Font("Monospaced", Font.PLAIN, 12)); String blankHTML = "<HTML><BODY><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>" + "</BODY></HTML>"; frameInfoPane = new JEditorPane("text/html", blankHTML); splitPane.setBottomComponent(new JScrollPane(frameInfoPane)); String label = "Profile Data: " + totalIntervals + " sample intervals " + "captured over " + totalDuration + " milliseconds"; DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(label, true); ProfileStackFrame[] theRootFrames = getRootFrames(); if (theRootFrames.length == 0) { System.err.println("ERROR: No data available for viewing."); return; } for (ProfileStackFrame frame : getRootFrames()) { boolean hasChildren = frame.hasSubFrames(); DefaultMutableTreeNode frameNode = new DefaultMutableTreeNode(frame, hasChildren); recurseTreeNodes(frame, frameNode); rootNode.add(frameNode); } profileTree = new JTree(new DefaultTreeModel(rootNode, true)); profileTree.setFont(new Font("Monospaced", Font.PLAIN, 12)); DefaultTreeSelectionModel selectionModel = new DefaultTreeSelectionModel(); selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); profileTree.setSelectionModel(selectionModel); profileTree.addTreeSelectionListener(this); profileTree.setSelectionPath(new TreePath(rootNode.getFirstChild())); valueChanged(null); splitPane.setTopComponent(new JScrollPane(profileTree)); splitPane.setResizeWeight(0.5); splitPane.setOneTouchExpandable(true); contentPane.add(splitPane, BorderLayout.CENTER); appWindow.pack(); appWindow.setVisible(true); } /** * Recursively adds subordinate nodes to the provided parent node with the * provided information. * * @param parentFrame The stack frame whose children are to be added as * subordinate nodes of the provided tree node. * @param parentNode The tree node to which the subordinate nodes are to be * added. */ private void recurseTreeNodes(ProfileStackFrame parentFrame, DefaultMutableTreeNode parentNode) { ProfileStackFrame[] subFrames = parentFrame.getSubordinateFrames(); if (subFrames.length == 0) { return; } for (ProfileStackFrame subFrame : subFrames) { boolean hasChildren = parentFrame.hasSubFrames(); DefaultMutableTreeNode subNode = new DefaultMutableTreeNode(subFrame, hasChildren); if (hasChildren) { recurseTreeNodes(subFrame, subNode); } parentNode.add(subNode); } } /** * Formats the provided count, padding with leading spaces as necessary. * * @param count The count value to be formatted. * @param length The total length for the string to return. * * @return The formatted count string. */ private String formatCount(long count, int length) { StringBuilder buffer = new StringBuilder(length); buffer.append(count); while (buffer.length() < length) { buffer.insert(0, ' '); } return buffer.toString(); } /** * Indicates that a node in the tree has been selected or deselected and that * any appropriate action should be taken. * * @param tse The tree selection event with information about the selection * or deselection that occurred. */ @Override public void valueChanged(TreeSelectionEvent tse) { try { TreePath path = profileTree.getSelectionPath(); if (path == null) { // Nothing is selected, so we'll use use an empty panel. frameInfoPane.setText(""); return; } DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent(); if (selectedNode == null) { // No tree node is selected, so we'll just use an empty panel. frameInfoPane.setText(""); return; } // It is possible that this is the root node, in which case we'll empty // the info pane. Object selectedObject = selectedNode.getUserObject(); if (! (selectedObject instanceof ProfileStackFrame)) { frameInfoPane.setText(""); return; } // There is a tree node selected, so we should convert it to a stack // frame and display information about it. ProfileStackFrame frame = (ProfileStackFrame) selectedObject; StringBuilder html = new StringBuilder(); html.append("<HTML><BODY><PRE>"); html.append("Information for stack frame <B>"); html.append(frame.getClassName()); html.append("."); html.append(frame.getHTMLSafeMethodName()); html.append("</B><BR><BR>Occurrences by Source Line Number:<BR>"); HashMap<Integer,Long> lineNumbers = frame.getLineNumbers(); for (Integer lineNumber : lineNumbers.keySet()) { html.append(" "); long count = lineNumbers.get(lineNumber); if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE) { html.append("<native>"); } else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) { html.append("<unknown>"); } else { html.append("Line "); html.append(lineNumber); } html.append(": "); html.append(count); if (count == 1) { html.append(" occurrence<BR>"); } else { html.append(" occurrences<BR>"); } } html.append("<BR><BR>"); html.append("<HR>Stack Traces Including this Method:"); String classAndMethod = frame.getClassName() + "." + frame.getMethodName(); HashMap<ProfileStack,Long> stacks = stacksByMethod.get(classAndMethod); for (ProfileStack stack : stacks.keySet()) { html.append("<BR><BR>"); html.append(stacks.get(stack)); html.append(" occurrence(s):"); appendHTMLStack(stack, html, classAndMethod); } html.append("</PRE></BODY></HTML>"); frameInfoPane.setText(html.toString()); frameInfoPane.setSelectionStart(0); frameInfoPane.setSelectionEnd(0); } catch (Exception e) { e.printStackTrace(); frameInfoPane.setText(""); } } /** * Appends an HTML representation of the provided stack to the given buffer. * * @param stack The stack trace to represent in HTML. * @param html The buffer to which the HTML version of * the stack should be appended. * @param highlightClassAndMethod The name of the class and method that * should be highlighted in the stack frame. */ private void appendHTMLStack(ProfileStack stack, StringBuilder html, String highlightClassAndMethod) { int numFrames = stack.getNumFrames(); for (int i=(numFrames-1); i >= 0; i--) { html.append("<BR> "); String className = stack.getClassName(i); String methodName = stack.getMethodName(i); int lineNumber = stack.getLineNumber(i); String safeMethod = (methodName.equals("<init>") ? "<init>" : methodName); String classAndMethod = className + "." + methodName; if (classAndMethod.equals(highlightClassAndMethod)) { html.append("<B>"); html.append(className); html.append("."); html.append(safeMethod); html.append(":"); if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE) { html.append("<native>"); } else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) { html.append("<unknown>"); } else { html.append(lineNumber); } html.append("</B>"); } else { html.append(className); html.append("."); html.append(safeMethod); html.append(":"); if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE) { html.append("<native>"); } else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) { html.append("<unknown>"); } else { html.append(lineNumber); } } } } }