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