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