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