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("<native>"); 573 } 574 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 575 { 576 html.append("<unknown>"); 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>") ? "<init>" : 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("<native>"); 664 } 665 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 666 { 667 html.append("<unknown>"); 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("<native>"); 686 } 687 else if (lineNumber == ProfileStack.LINE_NUMBER_UNKNOWN) 688 { 689 html.append("<unknown>"); 690 } 691 else 692 { 693 html.append(lineNumber); 694 } 695 } 696 } 697 } 698} 699