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("<native>"); 566 } 567 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 568 { 569 html.append("<unknown>"); 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>") ? "<init>" : 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("<native>"); 657 } 658 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 659 { 660 html.append("<unknown>"); 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("<native>"); 679 } 680 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 681 { 682 html.append("<unknown>"); 683 } 684 else 685 { 686 html.append(lineNumber); 687 } 688 } 689 } 690 } 691} 692