/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2012-2016 ForgeRock AS. */ package org.opends.server.plugins.profiler; 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 java.util.Map; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTree; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; 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 org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.io.ASN1; import org.forgerock.opendj.io.ASN1Reader; import com.forgerock.opendj.cli.ArgumentException; import com.forgerock.opendj.cli.ArgumentParser; import com.forgerock.opendj.cli.BooleanArgument; import com.forgerock.opendj.cli.StringArgument; import static org.opends.messages.PluginMessages.*; import static org.opends.messages.ToolMessages.*; import static org.opends.server.util.StaticUtils.*; import static com.forgerock.opendj.cli.CommonArguments.*; /** * 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 rootFrames; /** A set of stack traces indexed by class and method name. */ private HashMap> 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. LocalizableMessage 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 = StringArgument.builder("fileName") .shortIdentifier('f') .description(INFO_PROFILEVIEWER_DESCRIPTION_FILENAMES.get()) .multiValued() .required() .valuePlaceholder(INFO_FILE_PLACEHOLDER.get()) .buildAndAddToParser(argParser); useGUI = BooleanArgument.builder("useGUI") .shortIdentifier('g') .description(INFO_PROFILEVIEWER_DESCRIPTION_USE_GUI.get()) .buildAndAddToParser(argParser); displayUsage = showUsageArgument(); argParser.addArgument(displayUsage); argParser.setUsageArgument(displayUsage); } catch (ArgumentException ae) { LocalizableMessage 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) { argParser.displayMessageAndUsageReference(System.err, ERR_PROFILEVIEWER_ERROR_PARSING_ARGS.get(ae.getMessage())); System.exit(1); } // If we should just display usage or version 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) { LocalizableMessage 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 processDataFile */ public ProfileViewer() { rootFrames = new HashMap<>(); stacksByMethod = new HashMap<>(); 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. */ public void processDataFile(String filename) throws IOException { // 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 stackMap = stacksByMethod.get(classAndMethod); if (stackMap == null) { stackMap = new HashMap<>(); 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 { close(reader); } } /** * 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 = "









" + ""; 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); } } /** * 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.append("Information for stack frame ");
      html.append(frame.getClassName());
      html.append(".");
      html.append(frame.getHTMLSafeMethodName());
      html.append("

Occurrences by Source Line Number:
"); HashMap lineNumbers = frame.getLineNumbers(); for (Map.Entry entry : lineNumbers.entrySet()) { Integer lineNumber = entry.getKey(); html.append(" "); long count = entry.getValue(); 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
"); } else { html.append(" occurrences
"); } } html.append("

"); html.append("
Stack Traces Including this Method:"); String classAndMethod = frame.getClassName() + "." + frame.getMethodName(); HashMap stacks = stacksByMethod.get(classAndMethod); for (Map.Entry entry : stacks.entrySet()) { ProfileStack stack = entry.getKey(); html.append("

"); html.append(entry.getValue()); html.append(" occurrence(s):"); appendHTMLStack(stack, html, classAndMethod); } html.append("
"); 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("
"); String className = stack.getClassName(i); String methodName = stack.getMethodName(i); int lineNumber = stack.getLineNumber(i); String safeMethod = methodName.equals("") ? "<init>" : methodName; String classAndMethod = className + "." + methodName; if (classAndMethod.equals(highlightClassAndMethod)) { html.append(""); 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(""); } 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); } } } } }