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