blob: 31d80e13ba0cc485d4f1aef80394d83887bf8a0c [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.editors.hprof.views;
import com.android.tools.idea.actions.EditMultipleSourcesAction;
import com.android.tools.idea.actions.PsiFileAndLineNavigation;
import com.android.tools.idea.editors.allocations.ColumnTreeBuilder;
import com.android.tools.perflib.heap.*;
import com.intellij.debugger.ui.impl.tree.TreeBuilder;
import com.intellij.debugger.ui.impl.tree.TreeBuilderNode;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.PopupHandler;
import com.intellij.ui.RowIcon;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.components.JBList;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.PlatformIcons;
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.util.*;
import java.util.List;
public class InstanceReferenceTreeView implements DataProvider {
public static final String TREE_NAME = "HprofInstanceReferenceTree";
private static final int MAX_AUTO_EXPANSION_DEPTH = 5;
private static final SimpleTextAttributes SOFT_REFERENCE_TEXT_ATTRIBUTE =
new SimpleTextAttributes(SimpleTextAttributes.STYLE_ITALIC, XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES.getFgColor());
private static final Comparator<Instance> DEPTH_COMPARATOR = new Comparator<Instance>() {
@Override
public int compare(Instance o1, Instance o2) {
return o1.getDistanceToGcRoot() - o2.getDistanceToGcRoot();
}
};
@NotNull private Project myProject;
@NotNull private Tree myTree;
@NotNull private JComponent myColumnTree;
private Instance myInstance;
public InstanceReferenceTreeView(@NotNull Project project, @NotNull SelectionModel selectionModel) {
myProject = project;
final TreeBuilder model = new TreeBuilder(null) {
@Override
public void buildChildren(TreeBuilderNode node) {
if (node == getRoot()) {
node.add(new InstanceNode(this, myInstance));
}
else {
addReferences((InstanceNode)node);
}
nodeChanged(node);
}
@Override
public boolean isExpandable(TreeBuilderNode node) {
if (node == getRoot()) {
return node.getChildCount() > 0;
}
else {
Instance instance = (Instance)node.getUserObject();
return instance.getHardReferences().size() > 0 || instance.getSoftReferences() != null;
}
}
};
// Set the root to a dummy object since the TreeBuilder implementation is very buggy.
model.setRoot(new TreeBuilderNode(null) {
@Override
protected TreeBuilder getTreeBuilder() {
return model;
}
});
myTree = new Tree(model);
myTree.setName(TREE_NAME);
myTree.setRootVisible(false);
myTree.setShowsRootHandles(true);
myTree.setLargeModel(true);
myTree.addTreeExpansionListener(new TreeExpansionListener() {
private boolean myIsCurrentlyExpanding = false;
@Override
public void treeExpanded(TreeExpansionEvent event) {
if (myIsCurrentlyExpanding) {
return;
}
myIsCurrentlyExpanding = true;
InstanceNode node = (InstanceNode)event.getPath().getLastPathComponent();
InstanceNode currentNode = node;
int recursiveDepth = MAX_AUTO_EXPANSION_DEPTH;
while (currentNode.getChildCount() == 1 && recursiveDepth > 0) {
InstanceNode childNode = (InstanceNode)currentNode.getChildAt(0);
if (childNode.isLeaf() || childNode.getInstance().getDistanceToGcRoot() == 0) {
break;
}
currentNode = childNode;
Instance currentInstance = currentNode.getInstance();
--recursiveDepth;
if (currentInstance.getDistanceToGcRoot() == 0) {
break;
}
}
if (node != currentNode) {
myTree.expandPath(new TreePath(currentNode.getPath()));
}
myIsCurrentlyExpanding = false;
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
}
});
myTree.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, this);
JBList contextActionList = new JBList(new EditMultipleSourcesAction());
JBPopupFactory.getInstance().createListPopupBuilder(contextActionList);
final DefaultActionGroup popupGroup = new DefaultActionGroup(new EditMultipleSourcesAction());
myTree.addMouseListener(new PopupHandler() {
@Override
public void invokePopup(Component comp, int x, int y) {
ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, popupGroup).getComponent().show(comp, x, y);
}
});
ColumnTreeBuilder builder = new ColumnTreeBuilder(myTree)
.addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Reference Tree")
.setPreferredWidth(1200)
.setHeaderAlignment(SwingConstants.LEFT)
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof InstanceNode) {
InstanceNode node = (InstanceNode)value;
Instance instance = node.getInstance();
String[] referenceVarNames = node.getVarNames();
if (referenceVarNames.length > 0) {
if (instance instanceof ArrayInstance) {
append(StringUtil.pluralize("Index", referenceVarNames.length), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES);
append(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
StringBuilder builder = new StringBuilder();
builder.append(referenceVarNames[0]);
for (int i = 1; i < referenceVarNames.length; ++i) {
builder.append(", ");
builder.append(referenceVarNames[i]);
}
append(builder.toString(),
instance.getIsSoftReference() ? SOFT_REFERENCE_TEXT_ATTRIBUTE : XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES);
append(" in ", SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES);
}
SimpleTextAttributes classTextAttributes;
if (myInstance.getImmediateDominator() == instance) {
classTextAttributes = SimpleTextAttributes.SYNTHETIC_ATTRIBUTES;
}
else if (instance.getIsSoftReference()) {
classTextAttributes = SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES;
}
else if (instance.getDistanceToGcRoot() == 0) {
classTextAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
}
else if (instance.getImmediateDominator() == null) {
classTextAttributes = SimpleTextAttributes.ERROR_ATTRIBUTES;
}
else {
classTextAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
}
if (instance instanceof ArrayInstance) {
setIcon(AllIcons.Debugger.Db_array);
}
else if (instance instanceof ClassObj) {
setIcon(PlatformIcons.FIELD_ICON);
}
else {
setIcon(AllIcons.Debugger.Value);
}
if (myInstance.getImmediateDominator() == instance || instance.getDistanceToGcRoot() == 0) {
int totalIcons = 1 + (myInstance.getImmediateDominator() == instance ? 1 : 0) + (instance.getDistanceToGcRoot() == 0 ? 1 : 0);
RowIcon icons = new RowIcon(totalIcons);
icons.setIcon(getIcon(), 0);
int currentIcon = 1;
if (myInstance.getImmediateDominator() == instance) {
icons.setIcon(AllIcons.Hierarchy.Class, currentIcon++);
}
if (instance.getDistanceToGcRoot() == 0) {
icons.setIcon(AllIcons.Hierarchy.Subtypes, currentIcon);
}
setIcon(icons);
}
append(instance.toString(), classTextAttributes);
}
else {
append(value.toString(), SimpleTextAttributes.ERROR_ATTRIBUTES);
}
}
})
)
.addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Depth")
.setPreferredWidth(40)
.setHeaderAlignment(SwingConstants.RIGHT)
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof InstanceNode) {
Instance instance = ((InstanceNode)value).getInstance();
if (instance != null && instance.getDistanceToGcRoot() != Integer.MAX_VALUE) {
append(String.valueOf(instance.getDistanceToGcRoot()), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
setTextAlign(SwingConstants.RIGHT);
}
}
})
)
.addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Shallow Size")
.setPreferredWidth(80)
.setHeaderAlignment(SwingConstants.RIGHT)
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof InstanceNode) {
Instance instance = ((InstanceNode)value).getInstance();
if (instance != null) {
append(String.valueOf(instance.getSize()), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
setTextAlign(SwingConstants.RIGHT);
}
}
})
)
.addColumn(
new ColumnTreeBuilder.ColumnBuilder()
.setName("Dominating Size")
.setPreferredWidth(80)
.setHeaderAlignment(SwingConstants.RIGHT)
.setRenderer(new ColoredTreeCellRenderer() {
@Override
public void customizeCellRenderer(@NotNull JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
if (value instanceof InstanceNode) {
Instance instance = ((InstanceNode)value).getInstance();
if (instance != null && instance.getDistanceToGcRoot() != Integer.MAX_VALUE) {
append(String.valueOf(instance.getTotalRetainedSize()), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
setTextAlign(SwingConstants.RIGHT);
}
}
})
);
myColumnTree = builder.build();
selectionModel.addListener(new SelectionModel.SelectionListener() {
@Override
public void onHeapChanged(@NotNull Heap heap) {
clearInstance();
}
@Override
public void onClassObjChanged(@Nullable ClassObj classObj) {
clearInstance();
}
@Override
public void onInstanceChanged(@Nullable Instance instance) {
if (instance == null) {
clearInstance();
}
else {
myInstance = instance;
TreeBuilder model = getMutableModel();
TreeBuilderNode root = (TreeBuilderNode)model.getRoot();
root.removeAllChildren();
root.add(new InstanceNode(getMutableModel(), instance));
model.nodeStructureChanged((TreeBuilderNode)model.getRoot());
myTree.expandRow(0);
}
}
});
}
public JComponent getComponent() {
return myColumnTree;
}
private void clearInstance() {
TreeBuilderNode root = (TreeBuilderNode)getMutableModel().getRoot();
root.removeAllChildren();
getMutableModel().nodeStructureChanged(root);
}
@NotNull
private TreeBuilder getMutableModel() {
return (TreeBuilder)myTree.getModel();
}
private void addReferences(@NotNull InstanceNode node) {
Instance instance = node.getInstance();
if (instance instanceof RootObj) {
return;
}
List<Instance> sortedReferences = new ArrayList<Instance>(instance.getHardReferences());
Collections.sort(sortedReferences, DEPTH_COMPARATOR);
List<Instance> sortedSoftReferences;
if (instance.getSoftReferences() != null) {
sortedSoftReferences = new ArrayList<Instance>(instance.getSoftReferences());
Collections.sort(sortedSoftReferences, DEPTH_COMPARATOR);
sortedReferences.addAll(sortedSoftReferences); // Soft references should always appear after hard references.
}
for (Instance reference : sortedReferences) {
List<String> scratchList = new ArrayList<String>(3);
if (reference instanceof ClassInstance) {
ClassInstance classInstance = (ClassInstance)reference;
for (ClassInstance.FieldValue entry : classInstance.getValues()) {
if (entry.getField().getType() == Type.OBJECT && entry.getValue() == instance) {
scratchList.add(entry.getField().getName());
}
}
}
else if (reference instanceof ArrayInstance) {
ArrayInstance arrayInstance = (ArrayInstance)reference;
assert arrayInstance.getArrayType() == Type.OBJECT;
Object[] values = arrayInstance.getValues();
for (int i = 0; i < values.length; ++i) {
if (values[i] == instance) {
scratchList.add(String.valueOf(i));
}
}
}
else if (reference instanceof ClassObj) {
ClassObj classObj = (ClassObj)reference;
Map<Field, Object> staticValues = classObj.getStaticFieldValues();
for (Map.Entry<Field, Object> entry : staticValues.entrySet()) {
if (entry.getKey().getType() == Type.OBJECT && entry.getValue() == instance) {
scratchList.add(entry.getKey().getName());
}
}
}
String[] scratchNameArray = new String[scratchList.size()];
node.add(new InstanceNode(getMutableModel(), reference, scratchList.toArray(scratchNameArray)));
}
}
@Nullable
@Override
public Object getData(@NonNls String dataId) {
if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
return getTargetFiles();
}
else if (CommonDataKeys.PROJECT.is(dataId)) {
return myProject;
}
return null;
}
@Nullable
private PsiFileAndLineNavigation[] getTargetFiles() {
Object node = myTree.getSelectionPath().getLastPathComponent();
String className = null;
if (node instanceof InstanceNode) {
Instance instance = ((InstanceNode)node).getInstance();
if (instance instanceof ClassObj) {
className = ((ClassObj)instance).getClassName();
}
else {
className = instance.getClassObj().getClassName();
if (instance instanceof ArrayInstance) {
className = className.replace("[]", "");
}
}
}
return PsiFileAndLineNavigation.wrappersForClassName(myProject, className, 0);
}
private static class InstanceNode extends TreeBuilderNode {
@NotNull private TreeBuilder myModel;
@NotNull private String[] myVarNames;
public InstanceNode(@NotNull TreeBuilder model, @NotNull Instance userObject, @NotNull String... varNames) {
super(userObject);
myModel = model;
myVarNames = varNames;
}
@NotNull
public String[] getVarNames() {
return myVarNames;
}
public Instance getInstance() {
return (Instance)getUserObject();
}
@Override
protected TreeBuilder getTreeBuilder() {
return myModel;
}
}
}