001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.plugins.profiler;
018
019import java.awt.BorderLayout;
020import java.awt.Container;
021import java.awt.Font;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.util.Arrays;
025import java.util.HashMap;
026
027import javax.swing.JEditorPane;
028import javax.swing.JFrame;
029import javax.swing.JScrollPane;
030import javax.swing.JSplitPane;
031import javax.swing.JTree;
032import javax.swing.event.TreeSelectionEvent;
033import javax.swing.event.TreeSelectionListener;
034import javax.swing.tree.DefaultMutableTreeNode;
035import javax.swing.tree.DefaultTreeModel;
036import javax.swing.tree.DefaultTreeSelectionModel;
037import javax.swing.tree.TreePath;
038import javax.swing.tree.TreeSelectionModel;
039
040import org.forgerock.i18n.LocalizableMessage;
041import org.forgerock.opendj.io.ASN1;
042import org.forgerock.opendj.io.ASN1Reader;
043
044import com.forgerock.opendj.cli.ArgumentException;
045import com.forgerock.opendj.cli.ArgumentParser;
046import com.forgerock.opendj.cli.BooleanArgument;
047import com.forgerock.opendj.cli.StringArgument;
048
049import static org.opends.messages.PluginMessages.*;
050import static org.opends.messages.ToolMessages.*;
051import static org.opends.server.util.StaticUtils.*;
052import static com.forgerock.opendj.cli.CommonArguments.*;
053
054
055
056/**
057 * This class defines a Directory Server utility that may be used to view
058 * profile information that has been captured by the profiler plugin.  It
059 * supports viewing this information in either a command-line mode or using a
060 * simple GUI.
061 */
062public class ProfileViewer
063       implements TreeSelectionListener
064{
065  /** The root stack frames for the profile information that has been captured. */
066  private HashMap<ProfileStackFrame,ProfileStackFrame> rootFrames;
067
068  /** A set of stack traces indexed by class and method name. */
069  private HashMap<String,HashMap<ProfileStack,Long>> stacksByMethod;
070
071  /**
072   * The editor pane that will provide detailed information about the selected
073   * stack frame.
074   */
075  private JEditorPane frameInfoPane;
076
077  /** The GUI tree that will be used to hold stack frame information;. */
078  private JTree profileTree;
079
080  /** The total length of time in milliseconds for which data is available. */
081  private long totalDuration;
082
083  /** The total number of profile intervals for which data is available. */
084  private long totalIntervals;
085
086
087
088  /**
089   * Parses the command-line arguments and creates an instance of the profile
090   * viewer as appropriate.
091   *
092   * @param  args  The command-line arguments provided to this program.
093   */
094  public static void main(String[] args)
095  {
096    // Define the command-line arguments that may be used with this program.
097    BooleanArgument displayUsage;
098    BooleanArgument useGUI       = null;
099    StringArgument  fileNames    = null;
100
101
102    // Create the command-line argument parser for use with this program.
103    LocalizableMessage toolDescription = INFO_PROFILEVIEWER_TOOL_DESCRIPTION.get();
104    ArgumentParser argParser =
105         new ArgumentParser("org.opends.server.plugins.profiler.ProfileViewer",
106                            toolDescription, false);
107
108
109    // Initialize all the command-line argument types and register them with the parser
110    try
111    {
112      fileNames =
113              StringArgument.builder("fileName")
114                      .shortIdentifier('f')
115                      .description(INFO_PROFILEVIEWER_DESCRIPTION_FILENAMES.get())
116                      .multiValued()
117                      .required()
118                      .valuePlaceholder(INFO_FILE_PLACEHOLDER.get())
119                      .buildAndAddToParser(argParser);
120      useGUI =
121              BooleanArgument.builder("useGUI")
122                      .shortIdentifier('g')
123                      .description(INFO_PROFILEVIEWER_DESCRIPTION_USE_GUI.get())
124                      .buildAndAddToParser(argParser);
125
126      displayUsage = showUsageArgument();
127      argParser.addArgument(displayUsage);
128      argParser.setUsageArgument(displayUsage);
129    }
130    catch (ArgumentException ae)
131    {
132      LocalizableMessage message =
133              ERR_PROFILEVIEWER_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
134
135      System.err.println(message);
136      System.exit(1);
137    }
138
139
140    // Parse the command-line arguments provided to this program.
141    try
142    {
143      argParser.parseArguments(args);
144    }
145    catch (ArgumentException ae)
146    {
147      argParser.displayMessageAndUsageReference(System.err, ERR_PROFILEVIEWER_ERROR_PARSING_ARGS.get(ae.getMessage()));
148      System.exit(1);
149    }
150
151
152    // If we should just display usage or version information,
153    // then print it and exit.
154    if (argParser.usageOrVersionDisplayed())
155    {
156      System.exit(0);
157    }
158
159
160    // Create the profile viewer and read in the data files.
161    ProfileViewer viewer = new ProfileViewer();
162    for (String filename : fileNames.getValues())
163    {
164      try
165      {
166        viewer.processDataFile(filename);
167      }
168      catch (Exception e)
169      {
170        LocalizableMessage message =
171                ERR_PROFILEVIEWER_CANNOT_PROCESS_DATA_FILE.get(filename,
172                                    stackTraceToSingleLineString(e));
173        System.err.println(message);
174      }
175    }
176
177
178    // Write the captured information to standard output or display it in a GUI.
179    if (useGUI.isPresent())
180    {
181      viewer.displayGUI();
182    }
183    else
184    {
185      viewer.printProfileData();
186    }
187  }
188
189
190
191  /**
192   * Creates a new profile viewer object without any data.  It should be
193   * populated with one or more calls to <CODE>processDataFile</CODE>
194   */
195  public ProfileViewer()
196  {
197    rootFrames     = new HashMap<>();
198    stacksByMethod = new HashMap<>();
199    totalDuration  = 0;
200    totalIntervals = 0;
201  }
202
203
204
205  /**
206   * Reads and processes the information in the provided data file into this
207   * profile viewer.
208   *
209   * @param  filename  The path to the file containing the data to be read.
210   *
211   * @throws  IOException  If a problem occurs while trying to read from the
212   *                       data file.
213   */
214  public void processDataFile(String filename) throws IOException
215  {
216    // Try to open the file for reading.
217    ASN1Reader reader = ASN1.getReader(new FileInputStream(filename));
218
219
220    try
221    {
222      // The first element in the file must be a sequence with the header
223      // information.
224      reader.readStartSequence();
225      totalIntervals += reader.readInteger();
226
227      long startTime = reader.readInteger();
228      long stopTime  = reader.readInteger();
229      totalDuration += stopTime - startTime;
230      reader.readEndSequence();
231
232
233      // The remaining elements will contain the stack frames.
234      while (reader.hasNextElement())
235      {
236        ProfileStack stack = ProfileStack.decode(reader);
237
238        long count = reader.readInteger();
239
240        int pos = stack.getNumFrames() - 1;
241        if (pos < 0)
242        {
243          continue;
244        }
245
246        String[] classNames  = stack.getClassNames();
247        String[] methodNames = stack.getMethodNames();
248        int[]    lineNumbers = stack.getLineNumbers();
249
250        ProfileStackFrame frame = new ProfileStackFrame(classNames[pos],
251                                                        methodNames[pos]);
252
253        ProfileStackFrame existingFrame = rootFrames.get(frame);
254        if (existingFrame == null)
255        {
256          existingFrame = frame;
257        }
258
259        String classAndMethod = classNames[pos] + "." + methodNames[pos];
260        HashMap<ProfileStack,Long> stackMap =
261             stacksByMethod.get(classAndMethod);
262        if (stackMap == null)
263        {
264          stackMap = new HashMap<>();
265          stacksByMethod.put(classAndMethod, stackMap);
266        }
267        stackMap.put(stack, count);
268
269        existingFrame.updateLineNumberCount(lineNumbers[pos], count);
270        rootFrames.put(existingFrame, existingFrame);
271
272        existingFrame.recurseSubFrames(stack, pos-1, count, stacksByMethod);
273      }
274    }
275    finally
276    {
277      close(reader);
278    }
279  }
280
281
282
283  /**
284   * Retrieves an array containing the root frames for the profile information.
285   * The array will be sorted in descending order of matching stacks.  The
286   * elements of this array will be the leaf method names with sub-frames
287   * holding information about the callers of those methods.
288   *
289   * @return  An array containing the root frames for the profile information.
290   */
291  public ProfileStackFrame[] getRootFrames()
292  {
293    ProfileStackFrame[] frames = new ProfileStackFrame[0];
294    frames = rootFrames.values().toArray(frames);
295
296    Arrays.sort(frames);
297
298    return frames;
299  }
300
301
302
303  /**
304   * Retrieves the total number of sample intervals for which profile data is
305   * available.
306   *
307   * @return  The total number of sample intervals for which profile data is
308   *          available.
309   */
310  public long getTotalIntervals()
311  {
312    return totalIntervals;
313  }
314
315
316
317  /**
318   * Retrieves the total duration in milliseconds covered by the profile data.
319   *
320   * @return  The total duration in milliseconds covered by the profile data.
321   */
322  public long getTotalDuration()
323  {
324    return totalDuration;
325  }
326
327
328
329  /**
330   * Prints the profile information to standard output in a human-readable
331   * form.
332   */
333  public void printProfileData()
334  {
335    System.out.println("Total Intervals:     " + totalIntervals);
336    System.out.println("Total Duration:      " + totalDuration);
337
338    System.out.println();
339    System.out.println();
340
341    for (ProfileStackFrame frame : getRootFrames())
342    {
343      printFrame(frame, 0);
344    }
345  }
346
347
348
349  /**
350   * Prints the provided stack frame and its subordinates using the provided
351   * indent.
352   *
353   * @param  frame   The stack frame to be printed, followed by recursive
354   *                 information about all its subordinates.
355   * @param  indent  The number of tabs to indent the stack frame information.
356   */
357  private static void printFrame(ProfileStackFrame frame, int indent)
358  {
359    for (int i=0; i < indent; i++)
360    {
361      System.out.print("\t");
362    }
363
364    System.out.print(frame.getTotalCount());
365    System.out.print("\t");
366    System.out.print(frame.getClassName());
367    System.out.print(".");
368    System.out.println(frame.getMethodName());
369
370    if (frame.hasSubFrames())
371    {
372      for (ProfileStackFrame f : frame.getSubordinateFrames())
373      {
374        printFrame(f, indent+1);
375      }
376    }
377  }
378
379
380
381  /**
382   * Displays a simple GUI with the profile data.
383   */
384  public void displayGUI()
385  {
386    JFrame appWindow = new JFrame("Directory Server Profile Data");
387    appWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
388
389    JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
390
391    Container contentPane = appWindow.getContentPane();
392    contentPane.setLayout(new BorderLayout());
393    contentPane.setFont(new Font("Monospaced", Font.PLAIN, 12));
394
395    String blankHTML = "<HTML><BODY><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>" +
396                       "</BODY></HTML>";
397    frameInfoPane = new JEditorPane("text/html", blankHTML);
398    splitPane.setBottomComponent(new JScrollPane(frameInfoPane));
399
400    String label = "Profile Data:  " + totalIntervals + " sample intervals " +
401                   "captured over " + totalDuration + " milliseconds";
402    DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(label, true);
403
404    ProfileStackFrame[] theRootFrames = getRootFrames();
405    if (theRootFrames.length == 0)
406    {
407      System.err.println("ERROR:  No data available for viewing.");
408      return;
409    }
410
411    for (ProfileStackFrame frame : getRootFrames())
412    {
413      boolean hasChildren = frame.hasSubFrames();
414
415      DefaultMutableTreeNode frameNode =
416          new DefaultMutableTreeNode(frame, hasChildren);
417      recurseTreeNodes(frame, frameNode);
418
419      rootNode.add(frameNode);
420    }
421
422    profileTree = new JTree(new DefaultTreeModel(rootNode, true));
423    profileTree.setFont(new Font("Monospaced", Font.PLAIN, 12));
424
425    DefaultTreeSelectionModel selectionModel = new DefaultTreeSelectionModel();
426    selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
427    profileTree.setSelectionModel(selectionModel);
428    profileTree.addTreeSelectionListener(this);
429    profileTree.setSelectionPath(new TreePath(rootNode.getFirstChild()));
430    valueChanged(null);
431
432    splitPane.setTopComponent(new JScrollPane(profileTree));
433    splitPane.setResizeWeight(0.5);
434    splitPane.setOneTouchExpandable(true);
435    contentPane.add(splitPane, BorderLayout.CENTER);
436
437    appWindow.pack();
438    appWindow.setVisible(true);
439  }
440
441
442
443  /**
444   * Recursively adds subordinate nodes to the provided parent node with the
445   * provided information.
446   *
447   * @param  parentFrame  The stack frame whose children are to be added as
448   *                      subordinate nodes of the provided tree node.
449   * @param  parentNode   The tree node to which the subordinate nodes are to be
450   *                      added.
451   */
452  private void recurseTreeNodes(ProfileStackFrame parentFrame,
453                                DefaultMutableTreeNode parentNode)
454  {
455    ProfileStackFrame[] subFrames = parentFrame.getSubordinateFrames();
456    if (subFrames.length == 0)
457    {
458      return;
459    }
460
461
462    for (ProfileStackFrame subFrame : subFrames)
463    {
464      boolean hasChildren = parentFrame.hasSubFrames();
465
466      DefaultMutableTreeNode subNode =
467           new DefaultMutableTreeNode(subFrame, hasChildren);
468      if (hasChildren)
469      {
470        recurseTreeNodes(subFrame, subNode);
471      }
472
473      parentNode.add(subNode);
474    }
475  }
476
477  /**
478   * Indicates that a node in the tree has been selected or deselected and that
479   * any appropriate action should be taken.
480   *
481   * @param  tse  The tree selection event with information about the selection
482   *              or deselection that occurred.
483   */
484  @Override
485  public void valueChanged(TreeSelectionEvent tse)
486  {
487    try
488    {
489      TreePath path = profileTree.getSelectionPath();
490      if (path == null)
491      {
492        // Nothing is selected, so we'll use use an empty panel.
493        frameInfoPane.setText("");
494        return;
495      }
496
497
498      DefaultMutableTreeNode selectedNode =
499           (DefaultMutableTreeNode) path.getLastPathComponent();
500      if (selectedNode == null)
501      {
502        // No tree node is selected, so we'll just use an empty panel.
503        frameInfoPane.setText("");
504        return;
505      }
506
507
508      // It is possible that this is the root node, in which case we'll empty
509      // the info pane.
510      Object selectedObject = selectedNode.getUserObject();
511      if (! (selectedObject instanceof ProfileStackFrame))
512      {
513        frameInfoPane.setText("");
514        return;
515      }
516
517
518      // There is a tree node selected, so we should convert it to a stack
519      // frame and display information about it.
520      ProfileStackFrame frame = (ProfileStackFrame) selectedObject;
521
522      StringBuilder html = new StringBuilder();
523      html.append("<HTML><BODY><PRE>");
524      html.append("Information for stack frame <B>");
525      html.append(frame.getClassName());
526      html.append(".");
527      html.append(frame.getHTMLSafeMethodName());
528      html.append("</B><BR><BR>Occurrences by Source Line Number:<BR>");
529
530      HashMap<Integer,Long> lineNumbers = frame.getLineNumbers();
531      for (Integer lineNumber : lineNumbers.keySet())
532      {
533        html.append("     ");
534
535        long count = lineNumbers.get(lineNumber);
536
537        if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE)
538        {
539          html.append("&lt;native&gt;");
540        }
541        else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN)
542        {
543          html.append("&lt;unknown&gt;");
544        }
545        else
546        {
547          html.append("Line ");
548          html.append(lineNumber);
549        }
550
551        html.append(":  ");
552        html.append(count);
553
554        if (count == 1)
555        {
556          html.append(" occurrence<BR>");
557        }
558        else
559        {
560          html.append(" occurrences<BR>");
561        }
562      }
563
564      html.append("<BR><BR>");
565      html.append("<HR>Stack Traces Including this Method:");
566
567      String classAndMethod = frame.getClassName() + "." +
568                              frame.getMethodName();
569      HashMap<ProfileStack,Long> stacks = stacksByMethod.get(classAndMethod);
570
571      for (ProfileStack stack : stacks.keySet())
572      {
573        html.append("<BR><BR>");
574        html.append(stacks.get(stack));
575        html.append(" occurrence(s):");
576
577        appendHTMLStack(stack, html, classAndMethod);
578      }
579
580
581      html.append("</PRE></BODY></HTML>");
582
583      frameInfoPane.setText(html.toString());
584      frameInfoPane.setSelectionStart(0);
585      frameInfoPane.setSelectionEnd(0);
586    }
587    catch (Exception e)
588    {
589      e.printStackTrace();
590      frameInfoPane.setText("");
591    }
592  }
593
594
595
596  /**
597   * Appends an HTML representation of the provided stack to the given buffer.
598   *
599   * @param  stack                    The stack trace to represent in HTML.
600   * @param  html                     The buffer to which the HTML version of
601   *                                  the stack should be appended.
602   * @param  highlightClassAndMethod  The name of the class and method that
603   *                                  should be highlighted in the stack frame.
604   */
605  private void appendHTMLStack(ProfileStack stack, StringBuilder html,
606                               String highlightClassAndMethod)
607  {
608    int numFrames = stack.getNumFrames();
609    for (int i=numFrames-1; i >= 0; i--)
610    {
611      html.append("<BR>     ");
612
613      String className  = stack.getClassName(i);
614      String methodName = stack.getMethodName(i);
615      int    lineNumber = stack.getLineNumber(i);
616
617      String safeMethod = methodName.equals("<init>") ? "&lt;init&gt;" : methodName;
618
619      String classAndMethod = className + "." + methodName;
620      if (classAndMethod.equals(highlightClassAndMethod))
621      {
622        html.append("<B>");
623        html.append(className);
624        html.append(".");
625        html.append(safeMethod);
626        html.append(":");
627
628        if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE)
629        {
630          html.append("&lt;native&gt;");
631        }
632        else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN)
633        {
634          html.append("&lt;unknown&gt;");
635        }
636        else
637        {
638          html.append(lineNumber);
639        }
640
641        html.append("</B>");
642      }
643      else
644      {
645        html.append(className);
646        html.append(".");
647        html.append(safeMethod);
648        html.append(":");
649
650        if (lineNumber == ProfileStack.LINE_NUMBER_NATIVE)
651        {
652          html.append("&lt;native&gt;");
653        }
654        else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN)
655        {
656          html.append("&lt;unknown&gt;");
657        }
658        else
659        {
660          html.append(lineNumber);
661        }
662      }
663    }
664  }
665}
666