/*
* Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code 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 code 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 in the LICENSE file that
* accompanied this code).
*
* 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.max.vm.hosted;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import com.sun.max.lang.*;
import com.sun.max.program.*;
import com.sun.max.program.option.*;
import com.sun.max.unsafe.*;
import com.sun.max.util.*;
import com.sun.max.vm.hosted.GraphPrototype.Link;
import com.sun.max.vm.object.*;
/**
* A mechanism for saving, loading and printing the
* causality spanning-tree of the object graph in an {@linkplain BootImageGenerator image}.
*/
public final class BootImageObjectTree {
private BootImageObjectTree() {
}
/**
* The maximum length of an object's {@linkplain Object#toString() string} representation included in the saved
* tree.
*/
private static final int MAX_OBJECT_TOSTRING_LENGTH = 200;
/**
* The form of an object's {@linkplain Object#toString() string} representation included in the saved tree.
*
*/
enum TO_STRING_TAG {
/**
* An object that does not override {@link Object#toString()}.
*/
DEFAULT,
/**
* An object whose implementation of {@link Object#toString()} returns null.
*/
NULL,
/**
* An object whose implementation of {@link Object#toString()} returns a custom value.
*/
CUSTOM;
public static final List<TO_STRING_TAG> VALUES = Arrays.asList(values());
}
/**
* A node in the causality tree of the object graph. An edge from the object graph is a parent-child link in this
* tree if traversing that edge during {@linkplain GraphPrototype object graph serialization} caused the child to be
* added to the image.
*/
public static class Node {
private final String className;
private final int address;
private final long size;
private long aggregateSize = 0L;
private final String toString;
private List<Node> children;
private String parentLink;
/**
* Creates a new node with the specified class name, address, size, and verbose string.
*
* @param className the name of the class of this object
* @param address the address of this object
* @param size the size of this object in bytes
* @param toString the verbose string for this object
*/
public Node(String className, int address, long size, String toString) {
this.className = className;
this.address = address;
this.size = size;
this.toString = toString;
}
/**
* Adds a child to this node.
*
* @param child the child node
* @param link the name of the link that links this child to its parent, e.g.
* the name of an object field
*/
void addChild(Node child, String link) {
if (children == null) {
children = new ArrayList<Node>();
}
children.add(child);
child.parentLink = link;
}
/**
* Returns a sequence of the children of this node.
*
* @return a sequence of the children
*/
public List<Node> children() {
List<Node> empty = Collections.emptyList();
return children == null ? empty : children;
}
/**
* Prune this graph so that only nodes that match the specified predicate (and their
* parent nodes) are retained.
*
* @param predicate the predicate to apply to each node recursively
* @return {@code null} if the predicate does not apply to either this
* node or any child node; this node with the non-matching children removed if
* the predicate matches this node or a child node
*/
public Node prune(Predicate<Node> predicate) {
final Node pruned = new Node(className, address, size, toString);
pruned.parentLink = parentLink;
pruned.aggregateSize = aggregateSize();
if (children != null) {
for (Node child : children) {
final Node prunedChild = child.prune(predicate);
if (prunedChild != null) {
pruned.addChild(prunedChild, prunedChild.parentLink);
}
}
}
if (pruned.children != null || predicate.evaluate(this)) {
return pruned;
}
return null;
}
/**
* Computes the total size, including the size of the children.
*
* @return the size of this node plus the sum of the sizes of all its children nodes
*/
public long aggregateSize() {
if (aggregateSize == 0) {
aggregateSize = size;
for (Node child : children()) {
aggregateSize += child.aggregateSize();
}
}
return aggregateSize;
}
private static final String OTHER_SIBLING_PREFIX = "`-- ";
private static final String LAST_SIBLING_PREFIX = "|-- ";
private static final String OTHER_CHILD_INDENT = "| ";
private static final String LAST_CHILD_INDENT = " ";
/**
* Print this thee to the specified print writer.
*
* @param node the node to print
* @param showTreeLines a boolean indicating whether to print the tree lines
* @param printWriter the writer to which to print the output
* @param prefix the prefix to this node
* @param lastChild a boolean indicating whether to print the last child
* @param addressRadix the numbering radix for printing addresses (e.g. decimal or hexadecimal)
* @param relocation the address to add to each address for relocation
*/
private static void printTree(Node node, boolean showTreeLines, PrintWriter printWriter, String prefix, boolean lastChild, int addressRadix, long relocation) {
printWriter.println(prefix + (showTreeLines ? (!lastChild ? LAST_SIBLING_PREFIX : OTHER_SIBLING_PREFIX) : " ") + node.toString(addressRadix, relocation));
final List<Node> children = node.children();
if (!children.isEmpty()) {
final String childPrefix = prefix + (showTreeLines ? (lastChild ? LAST_CHILD_INDENT : OTHER_CHILD_INDENT) : " ");
for (final Iterator<Node> iterator = children.iterator(); iterator.hasNext();) {
final Node child = iterator.next();
printTree(child, showTreeLines, printWriter, childPrefix, !iterator.hasNext(), addressRadix, relocation);
}
}
}
/**
* Convert this node to a string.
* @return a string representation of this node
*/
@Override
public String toString() {
return toString(10, 0);
}
/**
* Convert this node to a string with the specified formatting.
* @param addressRadix the number radix for printing the address (e.g. decimal or hexadecimal)
* @param relocation the address to add to each address for relocation
* @return a string representation of this node
*/
public String toString(int addressRadix, long relocation) {
final StringBuilder sb = new StringBuilder();
if (parentLink != null) {
sb.append(parentLink).append(' ');
}
sb.append(className).append('@').append(Long.toString(address + relocation, addressRadix));
sb.append(" size=").append(size).append(" aggregateSize=").append(aggregateSize());
if (toString != null) {
sb.append(" toString=").append(toString.replace('\n', '0'));
}
return sb.toString();
}
/**
* Create a piece of GUI tree for this node and all of its children.
* @param radix the number radix for representing addresses
* @param relocation a value to add to addresses for relocation
* @return a JTree instance representing the object tree starting at this node
*/
public ObjectTreeNode buildTree(int radix, long relocation) {
if (++btc % 100000 == 0) {
Trace.line(1, "node: " + btc);
}
ObjectTreeNode n = new ObjectTreeNode(this);
addressToNodeMap().put(address, n);
if (children != null) {
for (Node child : children) {
n.add(child.buildTree(radix, relocation));
}
}
return n;
}
private static int btc = 0;
}
/**
* Saves the spanning tree of an object graph as represented by the links in the graph that were traversed.
*
* @param dataOutputStream
* where to save the tree
* @param links
* the traversed links of the graph that describe the tree to be saved
*/
public static void saveTree(DataOutputStream dataOutputStream, Set<Map.Entry<Object, Link>> links, Map<Object, Address> allocationMap) throws IOException {
final Map<Class, Integer> classPool = new HashMap<Class, Integer>();
final Map<Object, Integer> objectPool = new IdentityHashMap<Object, Integer>();
final Class[] classPoolIndices = new Class[links.size()];
final Object[] objectPoolIndices = new Object[links.size()];
int objectId = 0;
int classId = 0;
objectPool.put(null, objectId++);
for (Map.Entry<Object, Link> entry : links) {
final Object object = entry.getKey();
if (object != null) {
final Class< ? > clazz = object.getClass();
if (!classPool.containsKey(clazz)) {
classPoolIndices[classId] = clazz;
classPool.put(clazz, classId++);
}
objectPoolIndices[objectId] = object;
objectPool.put(object, objectId++);
}
}
assert objectId == objectPool.size();
assert classId == classPool.size();
Trace.line(1, "classes=" + classId + ", objects=" + objectId + ", links=" + links.size());
dataOutputStream.writeInt(classPool.size());
for (int i = 0; i != classPool.size(); ++i) {
if (i % 100 == 0) {
Trace.line(1, "class: " + i);
}
dataOutputStream.writeUTF(classPoolIndices[i].getName());
}
dataOutputStream.writeInt(objectId);
for (int i = 1; i < objectId; ++i) {
if (i % 100000 == 0) {
Trace.line(1, "object: " + i);
}
final Object object = objectPoolIndices[i];
final int address = allocationMap.get(object).toInt();
final long size = ObjectAccess.size(object).toLong();
String toString;
try {
toString = object.toString();
} catch (Exception e) {
toString = "<error calling toString()>: " + e.toString();
}
dataOutputStream.writeInt(classPool.get(object.getClass()));
dataOutputStream.writeInt(address);
dataOutputStream.writeLong(size);
if (toString == null) {
dataOutputStream.writeByte(TO_STRING_TAG.NULL.ordinal());
} else {
final String defaultToString = object.getClass().getName() + '@' + Integer.toHexString(object.hashCode());
if (toString.equals(defaultToString)) {
dataOutputStream.writeByte(TO_STRING_TAG.DEFAULT.ordinal());
} else {
dataOutputStream.writeByte(TO_STRING_TAG.CUSTOM.ordinal());
dataOutputStream.writeUTF(Strings.truncate(toString, MAX_OBJECT_TOSTRING_LENGTH));
}
}
}
int counter = 0;
for (Map.Entry<Object, Link> entry : links) {
final Object object = entry.getKey();
if (object != null) {
if (++counter % 100000 == 0) {
Trace.line(1, "link: " + counter);
}
final Link link = entry.getValue();
dataOutputStream.writeInt(objectPool.get(object));
if (link == null) {
dataOutputStream.writeInt(0);
} else {
final int parent = objectPool.get(link.parent);
final String name = link.name();
dataOutputStream.writeInt(parent);
dataOutputStream.writeUTF(name);
}
}
}
assert counter + 1 == objectId;
}
/**
* Loads a tree that was saved by {@linkplain #saveTree(DataOutputStream, Set, Map) this method}.
*
* @param dataInputStream
* a stream containing a saved tree
* @return the roots of the loaded tree
*/
public static Set<Node> loadTree(DataInputStream dataInputStream) throws IOException {
final Set<Node> roots = new HashSet<Node>();
final int classPoolSize = dataInputStream.readInt();
final String[] classNames = new String[classPoolSize];
for (int i = 0; i != classPoolSize; ++i) {
if (i % 100 == 0) {
Trace.line(1, "class: " + i);
}
final String className = dataInputStream.readUTF();
classNames[i] = className;
}
final int objectPoolSize = dataInputStream.readInt();
final Node[] objectPool = new Node[objectPoolSize];
for (int i = 1; i < objectPoolSize; ++i) {
if (i % 100000 == 0) {
Trace.line(1, "object: " + i);
}
final int classNameIndex = dataInputStream.readInt();
final int address = dataInputStream.readInt();
final long size = dataInputStream.readLong();
final String className = classNames[classNameIndex];
final TO_STRING_TAG tag = TO_STRING_TAG.VALUES.get(dataInputStream.readByte());
final String toString;
switch (tag) {
case DEFAULT:
case NULL:
toString = null;
break;
case CUSTOM:
toString = dataInputStream.readUTF();
break;
default:
throw ProgramError.unexpected();
}
objectPool[i] = new Node(className, address, size, toString);
}
for (int i = 1; i < objectPoolSize; ++i) {
if (i % 100000 == 0) {
Trace.line(1, "link: " + i);
}
final int objectIndex = dataInputStream.readInt();
final int parentIndex = dataInputStream.readInt();
final Node node = objectPool[objectIndex];
if (parentIndex != 0) {
objectPool[parentIndex].addChild(node, dataInputStream.readUTF());
} else {
roots.add(node);
}
}
return roots;
}
/**
* Prints the tree rooted at a given node to a given print writer. The format of the dump is similar to the output
* of the tree(1) utility that list contents of directories in a tree-like format.
*
* @param node
* the root of the tree to dump
* @param printWriter
* where to print the dump
* @param lastChild
* specifies if {@code node} is the last child of its parent that will be dumped
* @param addressRadix
* radix to use for printing addresses
* @param relocation
*/
public static void printTree(Node node, boolean showTreeLines, PrintWriter printWriter, boolean lastChild, int addressRadix, long relocation) {
Node.printTree(node, showTreeLines, printWriter, "", lastChild, addressRadix, relocation);
}
private static final OptionSet options = new OptionSet();
private static final Option<Integer> TRACE = options.newIntegerOption("trace", 1,
"selects the trace level of the tool");
private static final Option<File> INPUT_FILE = options.newFileOption("in", BootImageGenerator.getBootImageObjectTreeFile(null),
"the file from which to load the graph");
private static final Option<File> OUTPUT_FILE = options.newFileOption("out", getDefaultOutputFile(),
"the file to which the graph is printed");
private static final Option<String> FILTER = options.newStringOption("filter", null,
"filter for pruning the graph before printing");
private static final Option<Integer> RADIX = options.newIntegerOption("radix", 10,
"radix used for printing addresses");
private static final Option<Long> RELOC = options.newLongOption("reloc", 0L,
"relocation addresses by the specified amount while printing");
private static final Option<Boolean> SHOW_TREE_LINES = options.newBooleanOption("lines", true,
"show lines (instead of indentation only) to indicate relationships between nodes");
private static final Option<Boolean> HELP = options.newBooleanOption("help", false,
"show help message and exits.");
private static final Option<Boolean> GUI = options.newBooleanOption("view", false,
"view the object tree graphically instead of generating text");
/**
* Gets the default file name to which to output the image object tree.
* @return a file representing the default location for outputting the tree
*/
private static File getDefaultOutputFile() {
return new File(BootImageGenerator.getBootImageObjectTreeFile(null).getAbsolutePath() + ".txt");
}
/**
* Command line interface for loading a saved graph and printing it, optionally pruning it with a provided filter
* first.
*
* @param args
*/
public static void main(String[] args) throws IOException {
options.parseArguments(args);
if (HELP.getValue()) {
options.printHelp(System.out, 80);
return;
}
Trace.on(TRACE.getValue());
final InputStream inputStream = openInputFile();
if (inputStream == null) {
System.err.println("Cannot find input file " + INPUT_FILE.getValue().getAbsolutePath());
System.err.println("The input tree file is only created if the -tree option is used when creating the boot image.");
System.exit(1);
}
final DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(inputStream));
final Set<Node> roots = loadTree(dataInputStream);
inputStream.close();
if (FILTER.getValue() != null) {
final String filter = FILTER.getValue();
final Set<Node> prunedRoots = new HashSet<Node>(roots.size());
for (Node root : roots) {
final Node prunedRoot = root.prune(new Predicate<Node>() {
public boolean evaluate(Node object) {
return object.toString().contains(filter);
}
});
if (prunedRoot != null) {
prunedRoots.add(prunedRoot);
}
}
roots.clear();
roots.addAll(prunedRoots);
}
if (GUI.getValue()) {
viewInGUI(roots);
} else {
writeOutputFile(roots);
}
}
/**
* Create and show a GUI for viewing the object tree.
*/
private static void viewInGUI(final Set<Node> roots) {
// build tree and make the root objects shown
DefaultMutableTreeNode treeRoot = new DefaultMutableTreeNode("roots");
final JTree tree = new JTree(treeRoot);
for (Node root : roots) {
treeRoot.add(root.buildTree(RADIX.getValue(), RELOC.getValue()));
}
tree.expandRow(0);
tree.expandRow(1);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
// open GUI
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("Object Tree");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ObjectTreeView otv = new ObjectTreeView(tree);
frame.add(otv);
Dimension ss = Toolkit.getDefaultToolkit().getScreenSize();
frame.setSize((int) ss.getWidth(), (int) ss.getHeight());
frame.setVisible(true);
otv.split.setDividerLocation(0.75);
}
});
}
private static HashMap<Integer, ObjectTreeNode> addressToNode;
private static HashMap<Integer, ObjectTreeNode> addressToNodeMap() {
if (addressToNode == null) {
addressToNode = new HashMap<Integer, ObjectTreeNode>();
}
return addressToNode;
}
/**
* GUI class.
*/
static class ObjectTreeView extends JPanel {
JTextArea info;
JPanel navigation;
JTextField addressField;
JSplitPane split;
JTree tree;
ObjectTreeView(final JTree t) {
tree = t;
setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.gridwidth = GridBagConstraints.REMAINDER;
// enable info display
info = new JTextArea(10, 0);
info.setText("Select a tree node to have its information shown here.");
// handle selection
tree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
DefaultMutableTreeNode n = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (n == null) {
return;
}
if (n == tree.getModel().getRoot()) {
info.setText(n.toString());
} else {
info.setText(((ObjectTreeNode) n).getDetails());
}
}
});
// navigation controls
// controls for address and node inspection
navigation = new JPanel();
navigation.setLayout(new BoxLayout(navigation, BoxLayout.X_AXIS));
navigation.add(new JLabel("object address (decimal or 0x... hexadecimal): "));
addressField = new JTextField(10);
addressField.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// empty
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
jumpToNode();
}
}
@Override
public void keyReleased(KeyEvent e) {
// empty
}
});
navigation.add(addressField);
navigation.add(new JButton(new AbstractAction("navigate to object") {
@Override
public void actionPerformed(ActionEvent e) {
jumpToNode();
}
}));
// control for jumping to a node's parent
navigation.add(new JSeparator(SwingConstants.VERTICAL));
navigation.add(new JButton(new AbstractAction("jump to parent") {
@Override
public void actionPerformed(ActionEvent e) {
if (tree.getSelectionPath() == null) {
return;
}
TreePath path = tree.getSelectionPath().getParentPath();
if (path != null) {
tree.setSelectionPath(path);
tree.scrollPathToVisible(path);
}
}
}));
// control for resetting tree (back to start view)
navigation.add(new JSeparator(SwingConstants.VERTICAL));
navigation.add(new JButton(new AbstractAction("reset tree view") {
@Override
public void actionPerformed(ActionEvent e) {
for (int i = tree.getRowCount() - 1; i >= 0; i--) {
tree.collapseRow(i);
}
tree.expandRow(0);
tree.expandRow(1);
tree.setSelectionPath(null);
info.setText("");
}
}));
// assemble
c.fill = GridBagConstraints.HORIZONTAL;
add(navigation, c);
c.fill = GridBagConstraints.BOTH;
c.weightx = 1.0;
c.weighty = 1.0;
split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, new JScrollPane(tree), new JScrollPane(info));
add(split, c);
}
void jumpToNode() {
addressField.setBackground(Color.WHITE);
int address = 0;
boolean error = false;
try {
if (addressField.getText().startsWith("0x")) {
address = Integer.parseInt(addressField.getText().substring(2), 16);
} else {
address = Integer.parseInt(addressField.getText());
}
} catch (NumberFormatException nfe) {
error = true;
}
TreeNode node = null;
if (!error) {
node = addressToNodeMap().get(address);
if (node == null) {
error = true;
}
}
if (!error) {
Vector<TreeNode> path = new Vector<TreeNode>();
path.add(node);
do {
node = node.getParent();
path.add(0, node);
} while (node.getParent() != null);
TreePath treePath = new TreePath(path.toArray());
tree.setSelectionPath(treePath);
tree.scrollPathToVisible(treePath);
} else {
addressField.setBackground(Color.RED);
}
}
}
/**
* Class for representing object tree nodes in the GUI.
*/
static class ObjectTreeNode extends DefaultMutableTreeNode {
Node node;
String treeStringRep;
String detailStringRep;
ObjectTreeNode(Node node) {
this.node = node;
}
@Override
public String toString() {
if (treeStringRep == null) {
StringBuilder buf = new StringBuilder();
if (node.parentLink != null) {
buf.append(node.parentLink).append(" = ");
}
treeStringRep = buf.append(node.className).
append(" @ ").
append(Long.toString(node.address + RELOC.getValue(), 10)).
append(", 0x").
append(Long.toString(node.address + RELOC.getValue(), 16)).
toString();
}
return treeStringRep;
}
String getDetails() {
if (detailStringRep == null) {
detailStringRep = String.format(
"class: %s\naddress (decimal): %s\naddress (hexadecimal): %s\nsize: %d bytes\naggregate size: %s bytes",
node.className, Long.toString(node.address + RELOC.getValue(), 10), Long.toString(node.address + RELOC.getValue(), 16),
node.size, Longs.toUnitsString(node.aggregateSize(), false));
}
return detailStringRep;
}
}
/**
* Write the object tree to a file.
*
* @param roots
* @throws IOException
*/
private static void writeOutputFile(final Set<Node> roots) throws IOException {
final File outputFile = OUTPUT_FILE.getValue();
final Writer writer = new FileWriter(outputFile);
final PrintWriter printWriter = new PrintWriter(new BufferedWriter(writer)) {
private int counter;
@Override
public void println(String s) {
if (++counter % 100000 == 0) {
Trace.line(1, "node: " + counter);
}
super.println(s);
}
};
Trace.begin(1, "writing boot image object tree text file: " + outputFile.getAbsolutePath());
for (final Iterator<Node> iterator = roots.iterator(); iterator.hasNext();) {
final Node root = iterator.next();
Node.printTree(root, SHOW_TREE_LINES.getValue(), printWriter, "", !iterator.hasNext(), RADIX.getValue(), RELOC.getValue());
}
printWriter.close();
Trace.end(1, "writing boot image object tree text file: " + outputFile.getAbsolutePath());
}
private static FileInputStream openInputFile() {
try {
return new FileInputStream(INPUT_FILE.getValue());
} catch (FileNotFoundException e) {
return null;
}
}
}