/* * * * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ /* * */ import java.util.*; import java.io.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import javax.swing.*; import javax.swing.tree.*; import javax.swing.table.*; import javax.swing.event.*; import javax.swing.text.NumberFormatter; class Profile { /** * This hashtable is a collection of all CallGraphNodes in the callgraph. * It's indexed by each CallGraphNode's <b>index</b> member, whose type * is Integer. */ Hashtable nodes; /** * This hashtable is a collection of all CallGraphNodes in the callgraph. * It's indexed by each CallGraphNodes's <b>name</b>, whose type is * String. <p> * * Each entry in this hashtable is either a CallGraphNodes (if only * one CallGraphNode of that name exists in the callgraph), or * a Vector (if two or more CallGraphNodes of that name exist in * the callgraph). */ Hashtable nodesByName; /** * The root of all call records. This root is artificially created. * It's the parent of all the parent-less CallGraphNode read from * a profile. */ CallGraphNode root; /** * Number of CallGraphNodes in this profile that has one or more children, * including this.root. */ int numParents; /** * Number of graph files to parse. */ int numFiles; /** * Duration (in msec) of the period when the profile data was collected. */ double period; Profile(String[] files) throws IOException { nodes = new Hashtable(); nodesByName = new Hashtable(); root = createTopNode(null, -1, "root"); numParents = 0; numFiles = files.length; if (isMultifile()) { for (int i = 0; i < numFiles; i++) { readInputFile(new FileParserContext(files[i], i + 1, this)); } } else { readInputFile(new FileParserContext(files[0], 0, this)); } createCallGraph(); if (numParents > 0) { root.endTime = period; root.computeSummary(); } root.computePercentage(); } boolean isMultifile() { return numFiles > 1; } CallGraphNode createTopNode(CallGraphNode parent, int index, String name) { CallGraphNode r = new CallGraphNode(parent); r.parent = parent; r.parentIdx = parent == null ? null : parent.index; r.index = new Integer(index); r.name = name; nodes.put(r.index, r); addToIndexByName(r); return r; } void printRecord(CallGraphNode cgNode, String prefix) { if (cgNode.parent != null) { System.out.print(prefix); System.out.print(cgNode.name); System.out.print(", D=" + cgNode.depth); System.out.print(", K=" + cgNode.count); System.out.print(", C=" + cgNode.kidsCycles); System.out.print(", M=" + cgNode.kidsMsec); System.out.println(); prefix += " "; } if (cgNode.children != null) { Vector v = cgNode.children; for (int i=0; i<v.size(); i++) { printRecord((CallGraphNode)v.elementAt(i), prefix); } } } /** * Read the entire content of an input file */ void readInputFile(FileParserContext ctx) throws IOException { FileReader fr = new FileReader(ctx.file); BufferedReader reader = new BufferedReader(fr); String line; /* Skip the first line. It's for human consumption only */ line = reader.readLine(); //line = reader.readLine(); /* need revisit */ // Read all lines in the input file // while ((line = reader.readLine()) != null) { readInputLine(line, ctx); } } void readInputLine(String line, FileParserContext ctx) { StringTokenizer st = new StringTokenizer(line); if (st.countTokens() != 15) { return; } // Read the raw content of the input line into the CallGraphNode try { CallGraphNode cgNode; int index = Integer.parseInt(st.nextToken()); if (index == -1) { // We also have a predefined root node in our source file - // so modify the existing root node cgNode = ctx.root; st.nextToken(); // parent token is meaningless for the root } else { cgNode = new CallGraphNode(ctx.root); cgNode.index = new Integer(ctx.indexOffset + index); cgNode.parentIdx = new Integer(ctx.indexOffset + Integer.parseInt(st.nextToken())); } cgNode.depth = Integer.parseInt(st.nextToken()); cgNode.name = st.nextToken(); cgNode.count = Integer.parseInt(st.nextToken()); cgNode.onlyCycles = Long.parseLong(st.nextToken()); cgNode.onlyMsec = Double.parseDouble(st.nextToken()); cgNode.onlyPerc = Double.parseDouble(st.nextToken()); if (index != -1) { // kidsCylcles is calculated automatically for the root node cgNode.kidsCycles = Long.parseLong(st.nextToken()); cgNode.kidsMsec = Double.parseDouble(st.nextToken()); cgNode.kidsPerc = Double.parseDouble(st.nextToken()); // Timeline information cgNode.startTime = Double.parseDouble(st.nextToken()); cgNode.endTime = Double.parseDouble(st.nextToken()); if (cgNode.endTime > period) { period = cgNode.endTime; } // Store new CallGraphNode into the hashtable nodes.put(cgNode.index, cgNode); // Add it to another index table, searchable by cgNode.name addToIndexByName(cgNode); } } catch (NumberFormatException e) { // IMPL_NOTE: warning } } void createCallGraph() { // Now all the CallGraphNodes are created. We're ready to // construct the call graph for (Enumeration e = nodes.elements(); e.hasMoreElements() ;) { CallGraphNode cgNode = (CallGraphNode)e.nextElement(); if (cgNode.parentIdx == null) { // This is the root record. Do nothing } else { CallGraphNode parent; parent = (CallGraphNode)nodes.get(cgNode.parentIdx); if (parent == null) { System.out.println("WARNING: no parent found for element " + cgNode); parent = root; } if (parent.children == null) { parent.children = new Vector(); numParents ++; } parent.children.addElement(cgNode); cgNode.parent = parent; } } } /** * Add this cgNode to the index of all CallGraphNodes by its name. * This index is maintained by the hashtable <b>nodesByName</b> */ void addToIndexByName(CallGraphNode cgNode) { Object obj = nodesByName.get(cgNode.name); //System.out.println(cgNode.name + "=" + obj); if (obj == null) { // Not in the table yet, store the cgNode itself nodesByName.put(cgNode.name, cgNode); } else if (obj instanceof Vector) { // Already has two more more cgNodes of the same name, append // to the list ((Vector)obj).addElement(cgNode); } else { // We've seen the second cgNode of the same name. Store both // cgNodes inside a Vector and put that Vector in the hashtable. // // By doing this, we only create Vector for those method names // that correspond to more than one CallGraphNode Vector v = new Vector(2); v.addElement(obj); v.addElement(cgNode); nodesByName.put(cgNode.name, v); } } } class FileParserContext { /** * Name of the file being parsed. */ String file; /** * indexOffset is added to every node index found in a source file. * This helps to avoid collisions between equal indices in different * graph files. * indexOffset is defined as fileNo * MAX_INDEX * where MAX_INDEX is large enough to ensure that * every index in a source file is less than MAX_INDEX. */ int indexOffset; /** * The root node for all records of the file being parsed. */ CallGraphNode root; static final int MAX_INDEX = 10000000; FileParserContext(String fileName, int fileNo, Profile prof) { file = fileName; indexOffset = fileNo * MAX_INDEX; if (fileNo == 0) { // do not create additional level when parsing // exactly one source file; // all elements are attached directly to the "root" root = prof.root; } else { // create an auxiliary node named <fileName> which will be // the virtual root for all records in current file root = prof.createTopNode(prof.root, indexOffset - 1, fileName); } } } class Filter { boolean prefix; String substr; double startTime; double endTime; Filter(String text, double time) { if (text != null) { substr = text.trim(); if (substr.equals("")) { substr = null; } else if (prefix = substr.charAt(0) == '^') { substr = substr.substring(1); } } if (time >= 0.0) { startTime = time; endTime = time; } else { startTime = Double.MAX_VALUE; endTime = -1.0; } } boolean isEmpty() { return substr == null && endTime < 0.0; } boolean recordMatches(CallRecord r) { if (substr != null) { String name = r.name; if (name == null) { return false; } int n = name.indexOf(substr); if (n < 0 || (prefix && n != 0)) { return false; } } return r.startTime <= startTime && r.endTime >= endTime; } public boolean equals(Object o) { if (o == null) { return isEmpty(); } if (!(o instanceof Filter)) { return false; } Filter f = (Filter) o; if (substr == null) { if (f.substr != null) { return false; } } else { if (!substr.equals(f.substr) || prefix != f.prefix) { return false; } } return startTime == f.startTime && endTime == f.endTime; } } class CallRecord { final static int DEPTH = 0; final static int PARENT = 1; final static int NAME = 2; final static int COUNT = 3; final static int ONLY_CYCLES = 4; final static int AVG_CYCLES = 5; final static int ONLY_MSEC = 6; final static int ONLY_REL_PERC = 7; final static int ONLY_PERC = 8; final static int KIDS_CYCLES = 9; final static int KIDS_MSEC = 10; final static int KIDS_REL_PERC = 11; final static int KIDS_PERC = 12; final static int START_TIME = 13; final static int END_TIME = 14; final static int NUM_FIELDS = 15; String name; int count; long onlyCycles; long kidsCycles; double onlyMsec; double kidsMsec; double onlyPerc; double kidsPerc; double startTime; double endTime; // The topmost node that the record belongs to: // <root> for single-file profiles or <fileName> for multifile profiles. // It is used to calculate the percentage of the record via // dividing its onlyCycles or kidsCycles by owner.kidsCycles CallRecord owner; // If this record represents a delta record, deltaBase points to // the record which was compared to, otherwise deltaBase is null CallRecord deltaBase; CallRecord(CallRecord owner) { this.owner = owner == null ? this : owner; } void add(CallRecord other, boolean addKids) { if (other.startTime < this.startTime || this.count == 0) { this.startTime = other.startTime; } if (other.endTime > this.endTime) { this.endTime = other.endTime; } this.count += other.count; this.onlyCycles += other.onlyCycles; this.onlyMsec += other.onlyMsec; this.onlyPerc += other.onlyPerc; if (addKids) { this.kidsCycles += other.kidsCycles; this.kidsMsec += other.kidsMsec; this.kidsPerc += other.kidsPerc; } } void setDelta(CallRecord base) { if (deltaBase == null) { count -= base.count; onlyCycles -= base.onlyCycles; onlyMsec -= base.onlyMsec; onlyPerc -= base.onlyPerc; kidsCycles -= base.kidsCycles; kidsMsec -= base.kidsMsec; kidsPerc -= base.kidsPerc; startTime -= base.startTime; endTime -= base.endTime; deltaBase = base; } } void clearDelta() { if (deltaBase != null) { count += deltaBase.count; onlyCycles += deltaBase.onlyCycles; onlyMsec += deltaBase.onlyMsec; onlyPerc += deltaBase.onlyPerc; kidsCycles += deltaBase.kidsCycles; kidsMsec += deltaBase.kidsMsec; kidsPerc += deltaBase.kidsPerc; startTime += deltaBase.startTime; endTime += deltaBase.endTime; deltaBase = null; } } int depth; long getAvgOnlyCycles() { if (this.count <= 0) { return 0; } else { return this.onlyCycles / this.count; } } void computePercentage() { if (owner.kidsCycles == 0) { onlyPerc = 0; kidsPerc = 0; } else { onlyPerc = (double)onlyCycles / owner.kidsCycles; kidsPerc = (double)kidsCycles / owner.kidsCycles; } } } /** * AveragedCallRecord is used to support non-trivial merging of similar * records from different graph files like geometric averaging. */ class AveragedCallRecord extends CallRecord { static final int ARITHMETIC_MEAN = 0; static final int GEOMETRIC_MEAN = 1; static int averagingMethod = ARITHMETIC_MEAN; int multipliers; double onlyPercProduct; double kidsPercProduct; AveragedCallRecord(CallRecord owner) { super(owner); multipliers = 0; onlyPercProduct = 1; kidsPercProduct = 1; } void add(CallRecord other, boolean addKids) { super.add(other, addKids); multipliers++; if (owner != null && other.owner.kidsCycles != 0) { onlyPercProduct *= (double)other.onlyCycles / other.owner.kidsCycles; kidsPercProduct *= (double)other.kidsCycles / other.owner.kidsCycles; } } void computePercentage() { if (averagingMethod == ARITHMETIC_MEAN) { super.computePercentage(); return; } if (multipliers == 0) { onlyPerc = 0; kidsPerc = 0; } else { double power = 1 / (double)multipliers; onlyPerc = Math.pow(onlyPercProduct, power); kidsPerc = Math.pow(kidsPercProduct, power); } } } class CallGraphNode extends CallRecord { /** * The parent of this CallGraphNode. The parent/child relationship is * defined as: <pre> * if methodA calls methodB, and methodB calls methodC * then * CallGraphNode (methodA, methodB) is the parent of * CallGraphNode (methodB, methodC) * </pre> */ CallGraphNode parent; /** * A list of all CallGraphNodes whose parent is this CallGraphNode. * Is null of this CallGraphNode has no children. */ Vector children; /** * Is null iff this CallGraphNode has no children. */ CallRecord summary[]; Integer index; Integer parentIdx; CallGraphNode(CallRecord owner) { super(owner); } /** * Compute the summary of each CallGraphNodes that has at least one child. */ void computeSummary() { // (1) Merge the summary of all the children. We use a hashtable // to store the summary at this stage for faster searches. Vector v = children; Hashtable table = new Hashtable(); int i; /* Create myself */ CallRecord myself = getSummaryRecord(table, this.name); myself.add(this, true); for (i=0; i<v.size(); i++) { CallGraphNode child = (CallGraphNode)v.elementAt(i); if (child.children != null) { child.computeSummary(); for (int j=child.summary.length-1; j>=0; j--) { CallRecord chRec = child.summary[j]; CallRecord myRec = getSummaryRecord(table, chRec.name); if (!chRec.name.equals(this.name)) { myRec.add(chRec, true); } else { myRec.add(chRec, false); } } } else { CallRecord myRec = getSummaryRecord(table, child.name); if (!child.name.equals(this.name)) { myRec.add(child, true); } else { myRec.add(child, false); } } } // (2) convert the summary from a hashtable to a vector for // better size (use an array instead??) summary = new CallRecord[table.size()]; i = 0; for (Enumeration e = table.elements(); e.hasMoreElements() ;) { CallRecord rec = (CallRecord)e.nextElement(); summary[i++] = rec; } if (isTopNode()) { for (i=0; i<summary.length; i++) { CallRecord rec = summary[i]; myself.kidsCycles += rec.onlyCycles; myself.kidsMsec += rec.onlyMsec; myself.kidsPerc += rec.onlyPerc; } this.kidsCycles = myself.kidsCycles; this.kidsMsec = myself.kidsMsec; this.kidsPerc = myself.kidsPerc; } } boolean isTopNode() { // This node is global root or virtual file root // if and only if its index == -1 (mod MAX_INDEX) return (index.intValue() + 1) % FileParserContext.MAX_INDEX == 0; } void computePercentage() { // Compute percentage of this node, all of its subnodes // and summary records super.computePercentage(); if (children != null) { for (int i = 0; i < children.size(); i++) { ((CallGraphNode)children.elementAt(i)).computePercentage(); } } if (summary != null) { for (int i = 0; i < summary.length; i++) { summary[i].computePercentage(); } } } CallGraphNode findChildByName(String nameToFind) { for (int i = 0; i < children.size(); i++) { CallGraphNode child = (CallGraphNode)children.elementAt(i); if (child.name.equals(nameToFind)) { return child; } } return null; } CallRecord findSummaryByName(String nameToFind) { for (int i = 0; i < summary.length; i++) { if (summary[i].name.equals(nameToFind)) { return summary[i]; } } return null; } void setDelta(CallGraphNode base) { super.setDelta(base); if (children != null && base.children != null) { for (int i = 0; i < children.size(); i++) { CallGraphNode child = (CallGraphNode)children.elementAt(i); CallGraphNode prototype = base.findChildByName(child.name); if (prototype != null) { child.setDelta(prototype); } } } if (summary != null && base.summary != null) { for (int i = 0; i < summary.length; i++) { CallRecord prototype = base.findSummaryByName(summary[i].name); if (prototype != null) { summary[i].setDelta(prototype); } } } } void clearDelta() { super.clearDelta(); if (children != null) { for (int i = 0; i < children.size(); i++) { ((CallGraphNode)children.elementAt(i)).clearDelta(); } } if (summary != null) { for (int i = 0; i < summary.length; i++) { summary[i].clearDelta(); } } } CallRecord getSummaryRecord(Hashtable table, String name) { CallRecord rec = (CallRecord)table.get(name); if (rec == null) { CallRecord owner = isTopNode() ? this : this.owner; // Non-trivial averaging is currently supported for top-level // summary records except profile.root (for which name == this.name) if (parent == null && name != this.name) { rec = new AveragedCallRecord(owner); } else { rec = new CallRecord(owner); } rec.name = name; table.put(name, rec); } return rec; } public String toString() { if (parent == null) { return "-" + name; } else { return parent.name + "-" + name; } } } class MergedCallGraphNode extends CallGraphNode { MergedCallGraphNode(CallGraphNode src, int levels) { super(src.owner); if (levels > 0 && src.parent != null) { this.parent = new MergedCallGraphNode(src.parent, levels-1); } else { this.parent = new CallGraphNode(src.owner); this.parent.name = "...."; } this.name = src.name; this.add(src, true); } void add(CallGraphNode other, boolean addKids, int levels) { if (levels > 0) { MergedCallGraphNode myParent = (MergedCallGraphNode)this.parent; CallGraphNode otherParent = other.parent; if (myParent != null && other.parent != null) { myParent.add(otherParent, addKids, levels-1); } } super.add(other, addKids); } } class MergedCallRecord extends CallRecord { MergedCallRecord(CallRecord owner) { super(owner); } } abstract class BaseTableModel extends AbstractTableModel implements Comparator { JTable table; ProfView viewer; BaseTableModel(ProfView viewer) { this.viewer = viewer; } String names[] = { "Depth", "Parent", "Name", "Count", "Cycles", "Avg cycles", "Msec", "Rel %", "%", "Cycles_k", "Msec_k", "Rel %_k", "%_k", "Start", "End" }; boolean colVisible[] = new boolean[CallRecord.NUM_FIELDS]; void setOwner(JTable table) { this.table = table; setTableColumnWidths(); addMouseListenerToHeaderInTable(); } void setColumnVisible(int colName, boolean visible) { colVisible[colName] = visible; } /** * Convert the visible column (seen by user on the screen) to the * "real column", which is CallRecord.NAME, CallRecord.COUNT, etc. */ int toRealCol(int visCol) { for (int i=0; i<colVisible.length; i++) { if (colVisible[i]) { if (visCol == 0) { return i; } else { visCol --; } } } return 0; // shouldn't be here! } abstract CallRecord[] getDataArray(); abstract void setDataArray(CallRecord[] newArray); /** * Returns true if the contents of the data array are CallGraphNode * objects. */ abstract boolean isCallGraphNode(); CallRecord getRecordByRow(int row) { return getDataArray()[row]; } /** * Implements AbstractTableModel.getColumnCount(). */ public int getColumnCount() { int n = 0; for (int i=0; i<colVisible.length; i++) { if (colVisible[i]) { n ++; } } return n; } /** * Implements AbstractTableModel.getColumnName(). */ public String getColumnName(int visCol) { String name = names[toRealCol(visCol)]; return name; } /** * Implements AbstractTableModel.getRowCount(). */ public int getRowCount() { return getDataArray().length; } /** * Implements AbstractTableModel.getValueAt(). * * Get the object to display at (row, visCol) in the table. */ public Object getValueAt(int row, int visCol) { CallRecord[] dataArray = getDataArray(); CallRecord rec = dataArray[row]; switch (toRealCol(visCol)) { case CallRecord.PARENT: if (isCallGraphNode()) { // IMPL_NOTE: change to use (rec instanceof CallGraphNode) instead CallGraphNode parent = ((CallGraphNode)rec).parent; if (parent == null) { return "."; } else { return parent.name; } } else { return "??"; } case CallRecord.NAME: return rec.name; case CallRecord.COUNT: return new Integer(rec.count); case CallRecord.DEPTH: return new Integer(rec.depth); case CallRecord.ONLY_CYCLES: return new Long(rec.onlyCycles); case CallRecord.AVG_CYCLES: return new Long(rec.getAvgOnlyCycles()); case CallRecord.ONLY_MSEC: return new Double(rec.onlyMsec); case CallRecord.ONLY_REL_PERC: return getPercObject((double)rec.onlyCycles / viewer.currentTreeNode.cgNode.kidsCycles); case CallRecord.ONLY_PERC: return getPercObject(rec.onlyPerc); case CallRecord.KIDS_CYCLES: return new Long(rec.kidsCycles); case CallRecord.KIDS_MSEC: return new Double(rec.kidsMsec); case CallRecord.KIDS_REL_PERC: return getPercObject((double)rec.kidsCycles / viewer.currentTreeNode.cgNode.kidsCycles); case CallRecord.START_TIME: return new Double(rec.startTime); case CallRecord.END_TIME: return new Double(rec.endTime); case CallRecord.KIDS_PERC: default: return getPercObject(rec.kidsPerc); } } /** * Get the class of the object at the given visible column. */ public Class getColumnClass(int visCol) { switch (toRealCol(visCol)) { case CallRecord.PARENT: case CallRecord.NAME: return String.class; case CallRecord.COUNT: case CallRecord.DEPTH: return Integer.class; case CallRecord.ONLY_CYCLES: case CallRecord.KIDS_CYCLES: case CallRecord.AVG_CYCLES: return Long.class; case CallRecord.ONLY_PERC: case CallRecord.KIDS_PERC: case CallRecord.ONLY_REL_PERC: case CallRecord.KIDS_REL_PERC: return String.class; case CallRecord.ONLY_MSEC: case CallRecord.KIDS_MSEC: case CallRecord.START_TIME: case CallRecord.END_TIME: default: return Double.class; } } //---------------------------------------------------------------------- // // Table sorting // //---------------------------------------------------------------------- void addMouseListenerToHeaderInTable() { table.setColumnSelectionAllowed(false); MouseAdapter listMouseListener = new MouseAdapter() { public void mouseClicked(MouseEvent e) { TableColumnModel columnModel = table.getColumnModel(); int viewColumn = columnModel.getColumnIndexAtX(e.getX()); int column = table.convertColumnIndexToModel(viewColumn); if (e.getClickCount() == 1 && column != -1) { setSortColumn(column); sort(); updateTable(false); } } }; JTableHeader th = table.getTableHeader(); th.addMouseListener(listMouseListener); } /** * Implements Comparator.compare */ public int compare(Object o1, Object o2) { CallRecord rec1; CallRecord rec2; if (sortAscending) { rec1 = (CallRecord)o1; rec2 = (CallRecord)o2; } else { rec1 = (CallRecord)o2; rec2 = (CallRecord)o1; } switch (sortColumn) { case CallRecord.PARENT: if (isCallGraphNode()) { CallGraphNode parent1 = ((CallGraphNode)rec1).parent; CallGraphNode parent2 = ((CallGraphNode)rec2).parent; if (parent1 == null) { return -1; } else if (parent2 == null) { return 1; } else { return parent1.name.compareTo(parent2.name); } } else { return 0; } case CallRecord.NAME: return rec1.name.compareTo(rec2.name); case CallRecord.COUNT: if (rec1.count == rec2.count) return 0; if (rec1.count > rec2.count) return 1; if (rec1.count < rec2.count) return -1; case CallRecord.DEPTH: return rec1.depth - rec2.depth; case CallRecord.ONLY_PERC: if (rec1.onlyPerc == rec2.onlyPerc) return 0; if (rec1.onlyPerc > rec2.onlyPerc) return 1; if (rec1.onlyPerc < rec2.onlyPerc) return -1; case CallRecord.ONLY_CYCLES: case CallRecord.ONLY_MSEC: case CallRecord.ONLY_REL_PERC: if (rec1.onlyCycles == rec2.onlyCycles) return 0; if (rec1.onlyCycles > rec2.onlyCycles) return 1; if (rec1.onlyCycles < rec2.onlyCycles) return -1; case CallRecord.AVG_CYCLES: long rec1_avgCycles = rec1.getAvgOnlyCycles(); long rec2_avgCycles = rec2.getAvgOnlyCycles(); if (rec1_avgCycles == rec2_avgCycles) return 0; if (rec1_avgCycles > rec2_avgCycles) return 1; if (rec1_avgCycles < rec2_avgCycles) return -1; case CallRecord.KIDS_PERC: if (rec1.kidsPerc == rec2.kidsPerc) return 0; if (rec1.kidsPerc > rec2.kidsPerc) return 1; if (rec1.kidsPerc < rec2.kidsPerc) return -1; case CallRecord.START_TIME: if (rec1.startTime == rec2.startTime) return 0; if (rec1.startTime > rec2.startTime) return 1; if (rec1.startTime < rec2.startTime) return -1; case CallRecord.END_TIME: if (rec1.endTime == rec2.endTime) return 0; if (rec1.endTime > rec2.endTime) return 1; if (rec1.endTime < rec2.endTime) return -1; case CallRecord.KIDS_CYCLES: case CallRecord.KIDS_MSEC: case CallRecord.KIDS_REL_PERC: default: if (rec1.kidsCycles == rec2.kidsCycles) return 0; if (rec1.kidsCycles > rec2.kidsCycles) return 1; if (rec1.kidsCycles < rec2.kidsCycles) return -1; return 0; // will never happen, of course. } } protected int sortColumn = CallRecord.KIDS_CYCLES; protected boolean sortAscending = false; /** * Set the sorting column. If the same column is set twice consecutively, * the sorting direction is reversed. */ public void setSortColumn(int visCol) { int column = toRealCol(visCol); if (sortColumn != column) { sortColumn = column; if (column == CallRecord.NAME || column == CallRecord.PARENT) { sortAscending = true; } else { sortAscending = false; } } else { sortAscending = !sortAscending; } } /** * Sort the data array using the current sortColumn. */ public void sort() { CallRecord[] dataArray = getDataArray(); java.util.List l = Arrays.asList(dataArray); Collections.sort(l, this); setDataArray((CallRecord[])l.toArray((Object[])dataArray)); } /** * Export the current view of the table into a file. * IMPL_NOTE: make the file name selectable */ public void export() { try { CallRecord[] dataArray = getDataArray(); for (int i=0; i<dataArray.length; i++) { CallRecord cr = dataArray[i]; String name = cr.name; System.out.println("Precompile = " + name); } } catch (Throwable t) { t.printStackTrace(); } } //---------------------------------------------------------------------- // // Table Column Setup Frame // //---------------------------------------------------------------------- SetupFrame setupFrame; void setup() { if (setupFrame == null) { setupFrame = new SetupFrame(); } setupFrame.setVisible(true); setupFrame.toFront(); } class SetupFrame extends JFrame implements ActionListener { JCheckBox buttons[]; SetupFrame() { super("setup"); // Put the check boxes in a column in a panel JPanel checkPanel = new JPanel(); checkPanel.setLayout(new GridLayout(0, 1)); buttons = new JCheckBox[names.length]; for (int i=0; i<names.length; i++) { buttons[i] = new JCheckBox(names[i]); buttons[i].setSelected(colVisible[i]); checkPanel.add(buttons[i]); } JButton apply = new JButton("Apply"); apply.setActionCommand("apply"); apply.addActionListener(this); checkPanel.add(apply); this.getContentPane().add(checkPanel); this.pack(); this.setLocation(300, 100); this.setVisible(true); //setLayout(new BorderLayout()); //add(checkPanel, BorderLayout.WEST); //add(pictureLabel, BorderLayout.CENTER); //setBorder(BorderFactory.createEmptyBorder(20,20,20,20)); } public void actionPerformed(java.awt.event.ActionEvent e) { if (e.getActionCommand().equals("apply")) { boolean changed = false; for (int i=0; i<buttons.length; i++) { if (colVisible[i] != buttons[i].isSelected()) { changed = true ; colVisible[i] = buttons[i].isSelected(); } } if (changed) { updateTable(true); } setVisible(false); } } } void setTableColumnWidths() { if (this.table == null) { return; } TableColumnModel colModel = table.getColumnModel(); for (int realCol=0, visCol=0; realCol<names.length; realCol++) { if (!colVisible[realCol]) { continue; } int prefWidth; switch (realCol) { case CallRecord.PARENT: case CallRecord.NAME: prefWidth = 280; break; case CallRecord.COUNT: case CallRecord.ONLY_CYCLES: case CallRecord.AVG_CYCLES: case CallRecord.KIDS_CYCLES: prefWidth = 70; break; case CallRecord.DEPTH: case CallRecord.ONLY_MSEC: case CallRecord.ONLY_PERC: case CallRecord.ONLY_REL_PERC: prefWidth = 40; break; case CallRecord.KIDS_MSEC: case CallRecord.KIDS_PERC: case CallRecord.KIDS_REL_PERC: case CallRecord.START_TIME: case CallRecord.END_TIME: default: prefWidth = 50; } colModel.getColumn(visCol).setPreferredWidth(prefWidth); ++ visCol; } } public void updateTable(boolean colsChanged) { if (colsChanged) { // If the columns has changed, we must do the following // to force the table to refresh the column headers. table.setModel(new DefaultTableModel()); table.updateUI(); table.setModel(this); setTableColumnWidths(); } else { table.setModel(this); } table.updateUI(); } //---------------------------------------------------------------------- // // Percentage viewing // //---------------------------------------------------------------------- Object getPercObject(double val) { int ival = Math.abs((int)(val * 10000)); int i = ival / 100; int f = ival % 100; StringBuffer sbuf = new StringBuffer(); if (i < 100) { sbuf.append(" "); if (i < 10) { sbuf.append(" "); } } sbuf.append(val < 0 ? "-" : " "); sbuf.append(i); sbuf.append("."); if (f < 10) { sbuf.append("0"); } sbuf.append(f); return sbuf.toString(); } } class MainTableModel extends BaseTableModel { MainTableModel(ProfView viewer) { super(viewer); setColumnVisible(CallRecord.PARENT, false); setColumnVisible(CallRecord.NAME, true); setColumnVisible(CallRecord.COUNT, true); setColumnVisible(CallRecord.DEPTH, false); setColumnVisible(CallRecord.AVG_CYCLES, true); setColumnVisible(CallRecord.ONLY_CYCLES, true); setColumnVisible(CallRecord.ONLY_MSEC, true); setColumnVisible(CallRecord.ONLY_REL_PERC, false); setColumnVisible(CallRecord.ONLY_PERC, true); setColumnVisible(CallRecord.KIDS_CYCLES, true); setColumnVisible(CallRecord.KIDS_MSEC, true); setColumnVisible(CallRecord.KIDS_REL_PERC, false); setColumnVisible(CallRecord.KIDS_PERC, true); setColumnVisible(CallRecord.START_TIME, false); setColumnVisible(CallRecord.END_TIME, false); } boolean isCallGraphNode() { return false; } CallGraphNode cgNode; CallRecord noRecord[] = new CallRecord[0]; CallRecord singleRecord[] = new CallRecord[1]; CallRecord children[]; boolean showChildrenOnly; void setDataArray(CallRecord dataArray[]) { if (cgNode != null && cgNode.summary != null && dataArray != lastFilterResult) { if (showChildrenOnly) { children = dataArray; } else { cgNode.summary = dataArray; } } } /** * Restrict the TableModel to display only the CallRecords that are * reachable (directly or indirectly) from this cgNode. */ void setCurrentDataSet(CallGraphNode cgNode) { this.cgNode = cgNode; this.children = (cgNode == null || cgNode.children == null) ? null : (CallRecord[]) cgNode.children.toArray(noRecord); sort(); } Filter lastFilter; CallRecord lastFilterSrc[]; CallRecord lastFilterResult[]; CallRecord[] getDataArray() { CallRecord array[]; if (cgNode == null) { return noRecord; } else if (cgNode.summary == null) { singleRecord[0] = cgNode; array = singleRecord; } else if (showChildrenOnly) { array = children; } else { array = cgNode.summary; } if (viewer.filter != null) { array = filterData(array, viewer.filter); } return array; } CallRecord[] filterData(CallRecord src[], Filter filter) { if (src == lastFilterSrc && filter.equals(lastFilter)) { return lastFilterResult; } lastFilter = filter; lastFilterSrc = src; Vector v = new Vector(); for (int i = 0; i < src.length; i++) { CallRecord r = src[i]; if (filter.recordMatches(r)) { v.addElement(r); } } CallRecord result[]; if (src.length != v.size()) { if (v.size() == 0) { result = noRecord; } else { result = new CallRecord[v.size()]; v.toArray((Object[])result); } } else { result = src; } lastFilterResult = result; return result; } } class GotoFrame extends JFrame implements ListSelectionListener, ActionListener { Profile profile; JTable siteTable, stackTable; SiteTableModel siteTableModel; StackTableModel stackTableModel; ProfView viewer; JFormattedTextField levelText; GotoFrame(ProfView viewer, Profile profile) { this.viewer = viewer; this.profile = profile; Component sitePanel = createSitePanel(); Component stackPanel = createStackPanel(); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.setTopComponent(sitePanel); splitPane.setBottomComponent(stackPanel); this.getContentPane().add(splitPane, BorderLayout.CENTER); this.setLocation(70, 30); this.setSize(600, 500); this.pack(); } Component createSitePanel() { // The table siteTableModel = new SiteTableModel(viewer); siteTable = new ProfileTable(siteTableModel, this); siteTableModel.setOwner(siteTable); siteTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); ListSelectionModel rowSM = siteTable.getSelectionModel(); rowSM.addListSelectionListener(this); JScrollPane scrollPane = new JScrollPane(siteTable); scrollPane.setMinimumSize(new Dimension(180, 50)); scrollPane.setPreferredSize(new Dimension(600, 300)); // The button panel JPanel btnPanel = new JPanel(); btnPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); btnPanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2)); JCheckBox merge = new JCheckBox("Merge callers"); merge.setActionCommand("merge_callers"); merge.addActionListener(this); // Number of levels in merge JLabel levelLabel = new JLabel("Levels: ", JLabel.LEFT); levelLabel.setAlignmentX(Component.CENTER_ALIGNMENT); java.text.NumberFormat numberFormat = java.text.NumberFormat.getIntegerInstance(); NumberFormatter formatter = new NumberFormatter(numberFormat); formatter.setMinimum(new Integer(1)); formatter.setMaximum(new Integer(10000)); levelText = new JFormattedTextField(formatter); levelText.setValue(new Integer(1)); levelText.setColumns(3); levelText.addActionListener(this); btnPanel.add(levelLabel); btnPanel.add(levelText); btnPanel.add(merge); // put everything together JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(new JLabel("Call Sites"), BorderLayout.NORTH); panel.add(scrollPane, BorderLayout.CENTER); panel.add(btnPanel, BorderLayout.SOUTH); return panel; } Component createStackPanel() { // The table stackTableModel = new StackTableModel(viewer); stackTable = new ProfileTable(stackTableModel, this); stackTableModel.setOwner(stackTable); stackTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); //ListSelectionModel rowSM = stackTable.getSelectionModel(); //rowSM.addListSelectionListener(this); JScrollPane scrollPane = new JScrollPane(stackTable); scrollPane.setMinimumSize(new Dimension(180, 50)); scrollPane.setPreferredSize(new Dimension(600, 300)); // put everything together JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(new JLabel("Call Stack"), BorderLayout.NORTH); panel.add(scrollPane, BorderLayout.CENTER); return panel; } void display(String name) { siteTableModel.setCurrentDataSet(name); siteTableModel.updateTable(false); siteTableModel.sort(); ListSelectionModel rowSM = siteTable.getSelectionModel(); rowSM.clearSelection(); this.setTitle("All occurances of " + name); } /** * Handles Table selection changes. Implements * ListSelectionListener.valueChanged */ public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } ListSelectionModel lsm = (ListSelectionModel)e.getSource(); if (!lsm.isSelectionEmpty()) { int selectedRow = lsm.getMinSelectionIndex(); siteTableRowSelected(selectedRow); } } /** * User has selected an item in the site table. */ void siteTableRowSelected(int row) { CallGraphNode n = siteTableModel.nodes[row]; stackTableModel.setCurrentDataSet(n); stackTableModel.updateTable(false); ListSelectionModel lsm = stackTable.getSelectionModel(); lsm.clearSelection(); lsm.addSelectionInterval(0, 0); } class SiteTableModel extends BaseTableModel { String currentName = null; boolean merged = false; SiteTableModel(ProfView viewer) { super(viewer); setColumnVisible(CallRecord.PARENT, true); setColumnVisible(CallRecord.NAME, false); setColumnVisible(CallRecord.COUNT, true); setColumnVisible(CallRecord.DEPTH, true); setColumnVisible(CallRecord.ONLY_CYCLES, false); setColumnVisible(CallRecord.ONLY_MSEC, true); setColumnVisible(CallRecord.ONLY_REL_PERC, false); setColumnVisible(CallRecord.ONLY_PERC, true); setColumnVisible(CallRecord.KIDS_CYCLES, false); setColumnVisible(CallRecord.KIDS_MSEC, true); setColumnVisible(CallRecord.KIDS_REL_PERC, false); setColumnVisible(CallRecord.KIDS_PERC, true); setColumnVisible(CallRecord.START_TIME, false); setColumnVisible(CallRecord.END_TIME, false); setSortColumn(CallRecord.KIDS_PERC); } boolean isCallGraphNode() { return true; } /** * All CallGraphNode to be displayed in this window */ CallGraphNode nodes[] = new CallGraphNode[0]; CallRecord[] getDataArray() { return nodes; } void setDataArray(CallRecord dataArray[]) { nodes = (CallGraphNode[])dataArray; } /** * Restrict this TableModel to display only the CallRecords with * the given name. */ void setCurrentDataSet(String name) { Object obj = profile.nodesByName.get(name); if (obj == null) { this.nodes = new CallGraphNode[0]; } else if (obj instanceof CallGraphNode) { this.nodes = new CallGraphNode[1]; this.nodes[0] = (CallGraphNode)obj; } else { Vector v = (Vector)obj; if (merged) { v = mergeParents(v); } this.nodes = new CallGraphNode[v.size()]; v.toArray((Object[])nodes); } currentName = name; } void updateCurrentDataSet(boolean merged) { if (currentName != null) { this.merged = merged; setCurrentDataSet(currentName); } } boolean sameAncestors(CallGraphNode n1, CallGraphNode n2, int levels) { for (int i=0; i<levels; i++) { String name1 = null, name2 = null; if (n1.parent == null && n2.parent == null) { // both n1 and n2 are exactly the same call stack. Why // will this happen?? return true; } if (n1.parent != null) { name1 = n1.parent.name; } else { return false; } if (n2.parent != null) { name2 = n2.parent.name; } else { return false; } if (!name1.equals(name2)) { return false; } n1 = n1.parent; n2 = n2.parent; } return true; } /** * Create a list of "fake" parent nodes -- if function P may call * function C with different call stacks, create a new node that * combins all those nodes into a single node. This makes it easy * to tell how much time is spent in the P->C call, under all * circumstances */ Vector mergeParents(Vector nodes) { int mergeLevels = 3; int i, j; Vector mp = new Vector(); for (i=0; i<nodes.size(); i++) { CallGraphNode n1 = (CallGraphNode)nodes.elementAt(i); boolean merged = false; for (j=0; j<mp.size(); j++) { CallGraphNode n2 = (CallGraphNode)mp.elementAt(j); if (sameAncestors(n1, n2, mergeLevels)) { if (!(n2 instanceof MergedCallGraphNode)) { n2 = new MergedCallGraphNode(n2, mergeLevels); mp.set(j, n2); } ((MergedCallGraphNode)n2).add(n1, true, mergeLevels); merged = true; break; } } if (!merged) { mp.addElement(n1); } } return mp; } } class StackTableModel extends BaseTableModel { StackTableModel(ProfView viewer) { super(viewer); setColumnVisible(CallRecord.PARENT, false); setColumnVisible(CallRecord.NAME, true); setColumnVisible(CallRecord.COUNT, true); setColumnVisible(CallRecord.DEPTH, true); setColumnVisible(CallRecord.ONLY_CYCLES, false); setColumnVisible(CallRecord.ONLY_MSEC, true); setColumnVisible(CallRecord.ONLY_REL_PERC, false); setColumnVisible(CallRecord.ONLY_PERC, true); setColumnVisible(CallRecord.KIDS_CYCLES, false); setColumnVisible(CallRecord.KIDS_MSEC, true); setColumnVisible(CallRecord.KIDS_REL_PERC, false); setColumnVisible(CallRecord.KIDS_PERC, true); setColumnVisible(CallRecord.START_TIME, false); setColumnVisible(CallRecord.END_TIME, false); setSortColumn(CallRecord.DEPTH); } boolean isCallGraphNode() { return true; } /** * All CallGraphNode to be displayed in this window */ CallGraphNode nodes[] = new CallGraphNode[0]; CallRecord[] getDataArray() { return nodes; } void setDataArray(CallRecord dataArray[]) { nodes = (CallGraphNode[])dataArray; } /** * Restrict this TableModel to display only the CallRecords with * the given name. */ void setCurrentDataSet(CallGraphNode cgNode) { Vector v = new Vector(); for (; cgNode != null && cgNode.parent != null; cgNode = cgNode.parent) { v.addElement(cgNode); } nodes = new CallGraphNode[v.size()]; nodes = (CallGraphNode[])v.toArray((Object[])nodes); } } public void actionPerformed(java.awt.event.ActionEvent e) { String command = e.getActionCommand(); if (command.equals("merge_callers")) { JCheckBox cb = (JCheckBox)e.getSource(); siteTableModel.updateCurrentDataSet(cb.isSelected()); siteTableModel.updateTable(false); } else if (command.equals("table_dblclick")) { SyncCallGraph((ProfileTable)e.getSource()); }; } CallGraphNode getNodeFromTable(ProfileTable table, int row) { BaseTableModel dataModel = (BaseTableModel)table.getModel(); return ((CallGraphNode[])dataModel.getDataArray())[row]; } void SyncCallGraph(ProfileTable table) { ListSelectionModel lsm = table.getSelectionModel(); if (!lsm.isSelectionEmpty()) { int selectedRow = lsm.getMinSelectionIndex(); if (selectedRow >= 0) { CallGraphNode cgNode = getNodeFromTable(table, selectedRow); if (table == siteTable) { cgNode = cgNode.parent; } viewer.gotoGraph(cgNode); viewer.frame.toFront(); } } } } /** * This window displays all functions reachable from a function X, regardless * of the call stack of X. */ class CalleeFrame extends JFrame implements ActionListener { Profile profile; JTable calleeTable; CalleeTableModel calleeTableModel; ProfView viewer; CalleeFrame(ProfView viewer, Profile profile) { this.viewer = viewer; this.profile = profile; Component calleePanel = createCalleePanel(); this.getContentPane().add(calleePanel, BorderLayout.CENTER); this.setLocation(30, 80); this.setSize(600, 500); this.pack(); } Component createCalleePanel() { // The table calleeTableModel = new CalleeTableModel(viewer); calleeTable = new ProfileTable(calleeTableModel, null); calleeTableModel.setOwner(calleeTable); calleeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane scrollPane = new JScrollPane(calleeTable); scrollPane.setMinimumSize(new Dimension(180, 50)); scrollPane.setPreferredSize(new Dimension(600, 300)); // put everything together JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(new JLabel("Callees"), BorderLayout.NORTH); panel.add(scrollPane, BorderLayout.CENTER); return panel; } void display(String name) { calleeTableModel.setCurrentDataSet(name); calleeTableModel.updateTable(false); calleeTableModel.sort(); ListSelectionModel rowSM = calleeTable.getSelectionModel(); rowSM.clearSelection(); this.setTitle("All callees of " + name); } class CalleeTableModel extends BaseTableModel { String currentName = null; CalleeTableModel(ProfView viewer) { super(viewer); setColumnVisible(CallRecord.PARENT, false); setColumnVisible(CallRecord.NAME, true); setColumnVisible(CallRecord.COUNT, true); setColumnVisible(CallRecord.DEPTH, false); setColumnVisible(CallRecord.ONLY_CYCLES, false); setColumnVisible(CallRecord.ONLY_MSEC, true); setColumnVisible(CallRecord.ONLY_REL_PERC, false); setColumnVisible(CallRecord.ONLY_PERC, true); setColumnVisible(CallRecord.KIDS_CYCLES, false); setColumnVisible(CallRecord.KIDS_MSEC, true); setColumnVisible(CallRecord.KIDS_REL_PERC, false); setColumnVisible(CallRecord.KIDS_PERC, true); setColumnVisible(CallRecord.START_TIME, false); setColumnVisible(CallRecord.END_TIME, false); setSortColumn(CallRecord.KIDS_PERC); } boolean isCallGraphNode() { return false; } /** * All CallGraphNode to be displayed in this window */ CallRecord nodes[] = new CallRecord[0]; CallRecord[] getDataArray() { return nodes; } void setDataArray(CallRecord dataArray[]) { nodes = dataArray; } /** * Restrict this TableModel to display only the CallRecords with * the given name. */ void setCurrentDataSet(String name) { Object obj = profile.nodesByName.get(name); if (obj == null) { this.nodes = new CallRecord[0]; } else { Hashtable h = new Hashtable(); if (obj instanceof CallGraphNode) { addToDataSet(h, (CallGraphNode)obj); } else { Vector v = (Vector)obj; for (int i=0; i<v.size(); i++) { addToDataSet(h, (CallGraphNode)v.elementAt(i)); } } this.nodes = new CallRecord[h.size()]; int i=0; for (Enumeration e = h.elements(); e.hasMoreElements(); i++) { this.nodes[i] = (CallRecord)(e.nextElement()); } } currentName = name; } /** * Merge all callees of function X, regardless how we get to X. */ void addToDataSet(Hashtable h, CallGraphNode node) { if (node == null || node.summary == null) { return; } CallRecord summary[] = node.summary; for (int i=0; i<summary.length; i++) { CallRecord newRec = summary[i]; CallRecord oldRec = (CallRecord)h.get(newRec.name); if (oldRec == null) { h.put(newRec.name, newRec); } else { if (!(oldRec instanceof MergedCallRecord)) { MergedCallRecord mc = new MergedCallRecord(oldRec.owner); mc.name = oldRec.name; mc.add(oldRec, true); oldRec = mc; h.put(oldRec.name, oldRec); } oldRec.add(newRec, true); } } } } public void actionPerformed(java.awt.event.ActionEvent e) { String command = e.getActionCommand(); if (command.equals("setup_callee_table")) { calleeTableModel.setup(); } } } class MyTreeNode extends DefaultMutableTreeNode { CallGraphNode cgNode; MyTreeNode(CallGraphNode cgNode) { super(cgNode.name + " [" + cgNode.onlyCycles + "/" + cgNode.kidsCycles + "]"); this.cgNode = cgNode; } } class ProfileTable extends JTable implements ActionListener, MouseListener, TableCellRenderer { JPopupMenu popup; JRadioButtonMenuItem btnAbsolute, btnRelative, btnBoth, btnUnknown; JCheckBoxMenuItem btnTimeLine, btnChildrenOnly; ActionListener doubleClickListener; FontMetrics fm; TableColumnModel tcm; boolean showToolTip; TableCellRenderer renderer; void addMenuItem(String caption, String command) { JMenuItem item = popup.add(caption); item.setActionCommand(command); item.addActionListener(this); } JRadioButtonMenuItem addRadioMenuItem(ButtonGroup group, String caption, String command) { JRadioButtonMenuItem item = new JRadioButtonMenuItem(caption); item.setActionCommand(command); item.addActionListener(this); group.add(item); popup.add(item); return item; } JCheckBoxMenuItem addCheckBoxMenuItem(String caption, String command) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(caption); item.setActionCommand(command); item.addActionListener(this); popup.add(item); return item; } int isVisible(int column, int score) { return ((BaseTableModel)dataModel).colVisible[column] ? score : 0; } void maybePopup(MouseEvent e) { if (e.isPopupTrigger()) { switch(isVisible(CallRecord.ONLY_PERC, 1) + isVisible(CallRecord.KIDS_PERC, 2) + isVisible(CallRecord.ONLY_REL_PERC, 4) + isVisible(CallRecord.KIDS_REL_PERC, 8)) { case 3: btnAbsolute.setSelected(true); break; case 12: btnRelative.setSelected(true); break; case 15: btnBoth.setSelected(true); break; default: btnUnknown.setSelected(true); } popup.show(e.getComponent(), e.getX(), e.getY()); } } ProfileTable(AbstractTableModel model, ActionListener doubleClickListener) { super(model); fm = getFontMetrics(getFont()); tcm = getColumnModel(); this.doubleClickListener = doubleClickListener; addMouseListener(this); popup = new JPopupMenu(); addMenuItem("Columns...", "table_columns"); ButtonGroup group = new ButtonGroup(); btnAbsolute = addRadioMenuItem(group, "Absolute percentage", "percents"); btnRelative = addRadioMenuItem(group, "Relative percentage", "percents"); btnBoth = addRadioMenuItem(group, "Both", "percents"); group.add(btnUnknown = new JRadioButtonMenuItem("")); btnTimeLine = addCheckBoxMenuItem("Timeline data", "timeline"); if (model instanceof MainTableModel) { btnChildrenOnly = addCheckBoxMenuItem("Show children only", "children"); } popup.addSeparator(); addMenuItem("Export for precompilation", "table_precompile"); } public TableCellRenderer getCellRenderer(int row, int column) { renderer = super.getCellRenderer(row, column); return this; } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = renderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); CallRecord[] data = ((BaseTableModel)dataModel).getDataArray(); if (data[row].deltaBase == null) { c.setForeground(ProfView.DEFAULT_COLOR); } else { c.setForeground(ProfView.DELTA_COLOR); } return c; } public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); BaseTableModel tableModel = (BaseTableModel)dataModel; if (command.equals("table_columns")) { tableModel.setup(); } else if (command.equals("table_precompile")) { tableModel.export(); } else if (command.equals("percents")) { Object source = e.getSource(); boolean absolute = source == btnAbsolute || source == btnBoth; boolean relative = source == btnRelative || source == btnBoth; tableModel.setColumnVisible(CallRecord.ONLY_PERC, absolute); tableModel.setColumnVisible(CallRecord.KIDS_PERC, absolute); tableModel.setColumnVisible(CallRecord.ONLY_REL_PERC, relative); tableModel.setColumnVisible(CallRecord.KIDS_REL_PERC, relative); tableModel.updateTable(true); } else if (command.equals("timeline")) { boolean show = btnTimeLine.getState(); tableModel.setColumnVisible(CallRecord.START_TIME, show); tableModel.setColumnVisible(CallRecord.END_TIME, show); tableModel.updateTable(true); } else if (command.equals("children")) { ((MainTableModel)tableModel).showChildrenOnly = btnChildrenOnly.getState(); tableModel.updateTable(false); } } public void mouseClicked(MouseEvent e) { Point p = e.getPoint(); int row = rowAtPoint(p); int col = columnAtPoint(p); String s = getValueAt(row, col).toString(); Clipboard clp = Toolkit.getDefaultToolkit().getSystemSelection(); if (clp == null) { clp = Toolkit.getDefaultToolkit().getSystemClipboard(); } clp.setContents(new StringSelection(s), null); if (e.getClickCount() == 2 && doubleClickListener != null) { doubleClickListener.actionPerformed( new ActionEvent(this, 0, "table_dblclick", e.getModifiers()) ); } } public void mouseReleased(MouseEvent e) { maybePopup(e); } public void mousePressed(MouseEvent e) { maybePopup(e); } public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public String getToolTipText(MouseEvent e) { Point p = e.getPoint(); int row = rowAtPoint(p); int col = columnAtPoint(p); String s = getValueAt(row, col).toString(); showToolTip = fm.stringWidth(s) + 3 > tcm.getColumn(col).getWidth(); return showToolTip ? s : null; } public Point getToolTipLocation(MouseEvent e) { if (showToolTip) { Point p = e.getPoint(); int row = rowAtPoint(p); int col = columnAtPoint(p); return getCellRect(row, col, false).getLocation(); } else { return null; } } } class ProfileTreeCellRenderer extends DefaultTreeCellRenderer { ImageIcon iconJavaInt, iconJavaComp; static ImageIcon createImageIcon(String path) { java.net.URL imgURL = ProfileTreeCellRenderer.class.getResource(path); if (imgURL == null) { System.out.println("WARNING: Couldn't find resource: " + path); return null; } return new ImageIcon(imgURL); } ProfileTreeCellRenderer() { super(); iconJavaInt = createImageIcon("icons/java_int.gif"); iconJavaComp = createImageIcon("icons/java_comp.gif"); ImageIcon iconMethod = createImageIcon("icons/method.gif"); setOpenIcon(iconMethod); setClosedIcon(iconMethod); setLeafIcon(createImageIcon("icons/method_leaf.gif")); } public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus ); CallGraphNode cgNode = ((MyTreeNode)value).cgNode; if (cgNode.deltaBase == null) { setForeground(ProfView.DEFAULT_COLOR); } else { setForeground(ProfView.DELTA_COLOR); } if (cgNode.name.endsWith("<i>")) { setIcon(iconJavaInt); } else if (cgNode.name.endsWith("<c>")) { setIcon(iconJavaComp); } return this; } } class ProfileTree extends JTree implements MouseListener { boolean showToolTip; Rectangle rect; int indent; JPopupMenu popup; public ProfileTree(TreeNode root) { super(root); ProfileTreeCellRenderer renderer = new ProfileTreeCellRenderer(); indent = renderer.getOpenIcon().getIconWidth(); setCellRenderer(renderer); ToolTipManager.sharedInstance().registerComponent(this); popup = new JPopupMenu(); popup.add("Export to...").setActionCommand("tree_export"); popup.addSeparator(); popup.add("Compare to...").setActionCommand("tree_delta"); popup.add("Restore original values").setActionCommand("tree_restore"); addMouseListener(this); } public void addTreeListener(ActionListener listener) { MenuElement[] items = popup.getSubElements(); for (int i = 0; i < items.length; i++) { ((JMenuItem)items[i]).addActionListener(listener); } } public void mouseReleased(MouseEvent e) { maybePopup(e); } public void mousePressed(MouseEvent e) { maybePopup(e); } public void mouseClicked(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} void maybePopup(MouseEvent e) { if (e.isPopupTrigger()) { TreePath path = getPathForLocation(e.getX(), e.getY()); if (path != null) { setSelectionPath(path); popup.show(e.getComponent(), e.getX(), e.getY()); } } } public String getToolTipText(MouseEvent e) { TreePath path = getPathForLocation(e.getX(), e.getY()); if (path == null) { showToolTip = false; } else { rect = getPathBounds(path); showToolTip = rect.getX() + rect.getWidth() > getVisibleRect().getWidth(); } return showToolTip ? path.getLastPathComponent().toString() : null; } public Point getToolTipLocation(MouseEvent e) { if (showToolTip) { Point p = rect.getLocation(); p.translate(indent, 0); return p; } else { return null; } } } class ProfileWriter { CallGraphNode root; boolean normalize; int rootIndex; ProfileWriter(CallGraphNode root, boolean needToNormalize) { this.root = root; this.normalize = needToNormalize; this.rootIndex = root.index.intValue(); } // Normalizes indices of the graph nodes, // so the root node will always be -1 and the other nodes from // the multifile profile will obtain their original values int getNormalizedIndex(Integer idx) { int i = idx.intValue(); if (!normalize) { return i; } return i == rootIndex ? -1 : i % FileParserContext.MAX_INDEX; } void printNode(PrintStream out, CallGraphNode cgNode) { out.print(getNormalizedIndex(cgNode.index)); out.print('\t'); out.print(getNormalizedIndex(cgNode.parentIdx)); out.print('\t'); out.print(cgNode.depth); out.print('\t'); out.print(cgNode.name); out.print('\t'); out.print(cgNode.count); out.print('\t'); out.print(cgNode.onlyCycles); out.print('\t'); out.print(cgNode.onlyMsec); out.print('\t'); out.print(cgNode.onlyPerc); out.print('\t'); out.print(cgNode.kidsCycles); out.print('\t'); out.print(cgNode.kidsMsec); out.print('\t'); out.print(cgNode.kidsPerc); out.print('\t'); out.print(cgNode.startTime); out.print('\t'); out.print(cgNode.endTime); out.println("\t0\t0"); // dummies Vector v = cgNode.children; if (v != null) { for (int i = 0; i < v.size(); i++) { printNode(out, (CallGraphNode)v.elementAt(i)); } } } void exportTo(File file) { PrintStream out; try { out = new PrintStream(new FileOutputStream(file)); } catch (FileNotFoundException e) { e.printStackTrace(); return; } out.println("# Generated by ProfView"); printNode(out, root); out.close(); } } public class ProfView implements TreeSelectionListener, ListSelectionListener, DocumentListener, ActionListener, TimeLineListener { public static void main(String argv[]) { if (argv.length == 0) { System.out.println("No graph file specified!"); System.out.println("Usage: java -cp ProfView.jar ProfView graphfile1 ... graphfileN"); System.exit(1); } ProfView viewer = new ProfView(); viewer.start(argv); } final static Color DELTA_COLOR = new Color(0, 0, 160); final static Color DEFAULT_COLOR = Color.BLACK; JFrame frame; ProfileTree tree; JTable table; MainTableModel tableModel; GotoFrame gotoFrame; CalleeFrame calleeFrame; Profile profile; JLabel tableLabel; JCheckBox geomMean; MyTreeNode topTreeNode; MyTreeNode currentTreeNode; CallGraphNode deltaTarget; JFrame deltaPrompt; TimeLine timeline; javax.swing.Timer timer; JTextField filterText; Filter filter; void start(String[] files) { try { // UIManager.setLookAndFeel( // "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); this.profile = new Profile(files); String caption = files[0]; for (int i = 1; i < files.length; i++) { caption += " + " + files[i]; } frame = new JFrame("ProfView " + caption); JPanel treePanel = createTreePanel(profile); JPanel tablePanel = createTablePanel(profile); JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setTopComponent(treePanel); splitPane.setBottomComponent(tablePanel); frame.getContentPane().add(splitPane, BorderLayout.CENTER); frame.pack(); frame.setLocation(50, 50); frame.setSize(900, 600); frame.setVisible(true); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); } catch (Exception e) { e.printStackTrace(); } } JScrollPane createTableView(Profile profile) { tableModel = new MainTableModel(this); table = new ProfileTable(tableModel, this); tableModel.setCurrentDataSet(profile.root); tableModel.setOwner(table); JScrollPane tableView = new JScrollPane(table); tableView.setMinimumSize(new Dimension(180, 400)); tableView.setPreferredSize(new Dimension(680, 400)); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); ListSelectionModel rowSM = table.getSelectionModel(); rowSM.addListSelectionListener(this); return tableView; } JPanel createTablePanel(Profile profile) { // The table scroll pane JScrollPane tableView = createTableView(profile); // The button panel JPanel btnPanel = new JPanel(); GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); btnPanel.setLayout(gridbag); btnPanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2)); filterText = new JTextField(20); filterText.setActionCommand("filter_main_table"); filterText.addActionListener(this); filterText.getDocument().addDocumentListener(this); JLabel filterLabel = new JLabel("Filter:"); filterLabel.setLabelFor(filterText); // used for filter updating after filterText change timer = new javax.swing.Timer(500, this); JButton callee = new JButton("Callees"); callee.setActionCommand("callee_main_table"); callee.addActionListener(this); JButton caller = new JButton("Callers"); caller.setActionCommand("caller_main_table"); caller.addActionListener(this); timeline = new TimeLine(profile.period); timeline.setTimeLineListener(this); c.insets = new Insets(2, 2, 2, 2); c.fill = GridBagConstraints.HORIZONTAL; c.gridwidth = GridBagConstraints.REMAINDER; c.weightx = 1.0; gridbag.setConstraints(timeline, c); btnPanel.add(timeline); c.gridwidth = 1; c.weightx = 0.0; gridbag.setConstraints(filterLabel, c); btnPanel.add(filterLabel); c.weightx = 1.0; gridbag.setConstraints(filterText, c); btnPanel.add(filterText); c.weightx = 0.0; gridbag.setConstraints(callee, c); btnPanel.add(callee); gridbag.setConstraints(caller, c); btnPanel.add(caller); // Put them together JPanel tablePanel = new JPanel(); tablePanel.setLayout(new BorderLayout()); tableLabel = new JLabel("All calls under <root>"); tablePanel.add(tableLabel, BorderLayout.NORTH); tablePanel.add(btnPanel, BorderLayout.SOUTH); tablePanel.add(tableView, BorderLayout.CENTER); return tablePanel; } JPanel createTreePanel(Profile profile) { currentTreeNode = topTreeNode = createNodes(profile.root); tree = new ProfileTree(topTreeNode); tree.addTreeSelectionListener(this); tree.addTreeListener(this); tree.getSelectionModel().setSelectionMode (TreeSelectionModel.SINGLE_TREE_SELECTION); JScrollPane scrollPane = new JScrollPane(tree); scrollPane.setMinimumSize(new Dimension(180, 400)); scrollPane.setPreferredSize(new Dimension(600, 500)); JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(new JLabel("Call Graph"), BorderLayout.NORTH); panel.add(scrollPane, BorderLayout.CENTER); if (profile.isMultifile()) { // "Geom mean" checkbox is efficient only for multifile profiles geomMean = new JCheckBox("Merge using geom mean"); geomMean.setActionCommand("geom_mean_click"); geomMean.addActionListener(this); JPanel mergePanel = new JPanel(); mergePanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2)); mergePanel.add(geomMean); panel.add(mergePanel, BorderLayout.SOUTH); } return panel; } MyTreeNode createNodes(CallGraphNode cgNode) { MyTreeNode trNode = new MyTreeNode(cgNode); if (cgNode.children != null) { Vector v = cgNode.children; for (int i=0; i<v.size(); i++) { CallGraphNode child = (CallGraphNode)v.elementAt(i); trNode.add(createNodes(child)); } } return trNode; } /** * Handles Tree selection changes. */ public void valueChanged(TreeSelectionEvent e) { MyTreeNode n = (MyTreeNode) tree.getLastSelectedPathComponent(); if (n != currentTreeNode) { currentTreeNode = n; if (deltaTarget != null) { currentTreeNode.cgNode.setDelta(deltaTarget); deltaTarget = null; deltaPrompt.setVisible(false); } refreshTable(); } } void refreshTable() { if (currentTreeNode == null) { return; } tableLabel.setText("All calls under " + currentTreeNode.cgNode.name); tableModel.setCurrentDataSet(currentTreeNode.cgNode); tableModel.updateTable(false); ListSelectionModel rowSM = table.getSelectionModel(); rowSM.clearSelection(); } /** * Handles Table selection changes. Implements * ListSelectionListener.valueChanged */ public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } ListSelectionModel lsm = (ListSelectionModel)e.getSource(); if (!lsm.isSelectionEmpty()) { int selectedRow = lsm.getMinSelectionIndex(); tableRowSelected(selectedRow); } } void tableRowSelected(int row) { CallRecord rec = tableModel.getRecordByRow(row); timeline.select(rec.startTime, rec.endTime); if (gotoFrame != null && gotoFrame.isVisible()) { gotoFrame.display(rec.name); } if (calleeFrame != null && calleeFrame.isVisible()) { calleeFrame.display(rec.name); } } /** * Handles filter changes. Implements DocumentListener */ public void insertUpdate(javax.swing.event.DocumentEvent e) { timer.stop(); timer.start(); } public void removeUpdate(javax.swing.event.DocumentEvent e) { timer.stop(); timer.start(); } public void changedUpdate(javax.swing.event.DocumentEvent e) {} /** * Handle TimeLine actions */ public void markerChanged() { timer.stop(); timer.start(); } /** * Handle general actions */ public void actionPerformed(java.awt.event.ActionEvent e) { String command = e.getActionCommand(); // null means Timer event boolean alt = (e.getModifiers() & e.SHIFT_MASK) != 0; if (command == null || command.equals("filter_main_table")) { timer.stop(); Filter f = new Filter(filterText.getText(), timeline.getMarkedTime()); if (!f.equals(filter)) { filter = f.isEmpty() ? null : f; refreshTable(); } } else if (command.equals("caller_main_table") || command.equals("table_dblclick") && !alt) { if (gotoFrame == null) { gotoFrame = new GotoFrame(this, profile); } if (!gotoFrame.isVisible()) { gotoFrame.setVisible(true); ListSelectionModel lsm = table.getSelectionModel(); if (!lsm.isSelectionEmpty()) { tableRowSelected(lsm.getMinSelectionIndex()); } } ListSelectionModel lsm = gotoFrame.siteTable.getSelectionModel(); lsm.clearSelection(); lsm.addSelectionInterval(0, 0); gotoFrame.toFront(); } else if (command.equals("callee_main_table") || command.equals("table_dblclick") && alt) { if (calleeFrame == null) { calleeFrame = new CalleeFrame(this, profile); } if (!calleeFrame.isVisible()) { calleeFrame.setVisible(true); ListSelectionModel lsm = table.getSelectionModel(); if (!lsm.isSelectionEmpty()) { tableRowSelected(lsm.getMinSelectionIndex()); } } calleeFrame.toFront(); } else if (command.equals("geom_mean_click")) { AveragedCallRecord.averagingMethod = ((JCheckBox)e.getSource()).isSelected() ? AveragedCallRecord.GEOMETRIC_MEAN : AveragedCallRecord.ARITHMETIC_MEAN; profile.root.computePercentage(); refreshTable(); } else if (command.equals("tree_export")) { File file = chooseFileName( "Export " + currentTreeNode.cgNode.name + " subtree to"); if (file != null) { boolean needToNormalize = currentTreeNode != topTreeNode; (new ProfileWriter(currentTreeNode.cgNode, needToNormalize)). exportTo(file); } } else if (command.equals("tree_delta")) { deltaTarget = currentTreeNode.cgNode; showDeltaPrompt(); } else if (command.equals("tree_cancel")) { deltaTarget = null; deltaPrompt.setVisible(false); } else if (command.equals("tree_restore")) { currentTreeNode.cgNode.clearDelta(); refreshTable(); } } void showDeltaPrompt() { if (deltaPrompt == null) { deltaPrompt = new JFrame("Calculate profiles Delta"); JButton cancel = new JButton("Cancel"); cancel.setActionCommand("tree_cancel"); cancel.addActionListener(this); Container pane = deltaPrompt.getContentPane(); pane.setLayout(new GridLayout(2, 1)); pane.add(new JLabel(" Please, click the tree node" + " that will be compared to the selection ")); pane.add(cancel); deltaPrompt.pack(); deltaPrompt.setLocation(300, 100); } deltaPrompt.setVisible(true); } TreePath getTreeTath(CallGraphNode cgNode) { if (cgNode.parent == null) { return new TreePath(topTreeNode); } else { TreePath treePath = getTreeTath(cgNode.parent); MyTreeNode parentTreeNode = (MyTreeNode)treePath.getLastPathComponent(); for (Enumeration e = parentTreeNode.children(); e.hasMoreElements();) { MyTreeNode treeNode = (MyTreeNode)e.nextElement(); if (treeNode.cgNode == cgNode) { return treePath.pathByAddingChild(treeNode); } } throw new RuntimeException("Shouldn't come to here!"); } } void gotoGraph(CallGraphNode cgNode) { TreePath treePath = getTreeTath(cgNode); tree.scrollPathToVisible(treePath); tree.setSelectionPath(treePath); } File chooseFileName(String caption) { JFileChooser filebox = new JFileChooser(); filebox.setDialogTitle(caption); if (filebox.showSaveDialog(frame) == filebox.APPROVE_OPTION) { return filebox.getSelectedFile(); } return null; } }