diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ClassesActionBase.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ClassesActionBase.java new file mode 100644 index 000000000000..e534614afd83 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ClassesActionBase.java @@ -0,0 +1,38 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ReferenceType; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.view.ClassesTable; + +public abstract class ClassesActionBase extends AnAction { + @Override + public void update(AnActionEvent e) { + e.getPresentation().setEnabled(isEnabled(e)); + } + + @Override + public void actionPerformed(AnActionEvent e) { + perform(e); + } + + protected boolean isEnabled(AnActionEvent e) { + Project project = e.getProject(); + return project != null && !project.isDisposed(); + } + + protected abstract void perform(AnActionEvent e); + + @Nullable + protected ReferenceType getSelectedClass(AnActionEvent e) { + return e.getData(ClassesTable.SELECTED_CLASS_KEY); + } + + @Nullable + XDebugSession getDebugSession(AnActionEvent e) { + return e.getData(ClassesTable.DEBUG_SESSION_KEY); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/DebuggerTreeAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/DebuggerTreeAction.java new file mode 100644 index 000000000000..c46d72c5ff97 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/DebuggerTreeAction.java @@ -0,0 +1,28 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.debugger.ui.impl.watch.NodeDescriptorProvider; +import com.intellij.debugger.ui.tree.NodeDescriptor; +import com.intellij.debugger.ui.tree.ValueDescriptor; +import com.intellij.xdebugger.frame.XValue; +import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class DebuggerTreeAction extends XDebuggerTreeActionBase { + @Nullable + protected ObjectReference getObjectReference(@NotNull XValueNodeImpl node) { + XValue valueContainer = node.getValueContainer(); + if (valueContainer instanceof NodeDescriptorProvider) { + NodeDescriptor descriptor = ((NodeDescriptorProvider) valueContainer).getDescriptor(); + if (descriptor instanceof ValueDescriptor) { + Value value = ((ValueDescriptor)descriptor).getValue(); + return value instanceof ObjectReference ? (ObjectReference) value : null; + } + } + + return null; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/EnableBackgroundTrackingAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/EnableBackgroundTrackingAction.java new file mode 100644 index 000000000000..f475aa11e339 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/EnableBackgroundTrackingAction.java @@ -0,0 +1,23 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.openapi.project.Project; +import org.jetbrains.debugger.memory.component.InstancesTracker; + +public class EnableBackgroundTrackingAction extends ToggleAction{ + + @Override + public boolean isSelected(AnActionEvent e) { + Project project = e.getProject(); + return project != null && InstancesTracker.getInstance(project).isBackgroundTrackingEnabled(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + Project project = e.getProject(); + if (project != null) { + InstancesTracker.getInstance(project).setBackgroundTackingEnabled(state); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/JumpToTypeSourceAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/JumpToTypeSourceAction.java new file mode 100644 index 000000000000..6e72daadb9ff --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/JumpToTypeSourceAction.java @@ -0,0 +1,66 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.codeInsight.navigation.NavigationUtil; +import com.intellij.debugger.engine.DebuggerUtils; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiClass; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class JumpToTypeSourceAction extends ClassesActionBase { + + @Override + protected boolean isEnabled(AnActionEvent e) { + final PsiClass psiClass = getPsiClass(e); + + return super.isEnabled(e) && psiClass != null && psiClass.isPhysical(); + } + + @Override + protected void perform(AnActionEvent e) { + final PsiClass psiClass = getPsiClass(e); + if (psiClass != null) { + NavigationUtil.activateFileWithPsiElement(psiClass); + } + } + + @Nullable + private PsiClass getPsiClass(AnActionEvent e) { + ReferenceType selectedClass = getSelectedClass(e); + XDebugSession session = getDebugSession(e); + if (selectedClass == null || session == null) { + return null; + } + + ReferenceType targetClass = getObjectType(selectedClass); + if (targetClass != null) { + Project project = session.getProject(); + return DebuggerUtils + .findClass(targetClass.name(), project, GlobalSearchScope.allScope(project)); + } + + return null; + } + + @Nullable + private ReferenceType getObjectType(@NotNull ReferenceType ref) { + if (!(ref instanceof ArrayType)) { + return ref; + } + + String elementTypeName = ref.name().replace("[]", ""); + VirtualMachine vm = ref.virtualMachine(); + final List referenceTypes = vm.classesByName(elementTypeName); + if (referenceTypes.size() == 1) { + return referenceTypes.get(0); + } + + return null; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithDiffAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithDiffAction.java new file mode 100644 index 000000000000..f12c349e0593 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithDiffAction.java @@ -0,0 +1,21 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.openapi.project.Project; +import org.jetbrains.debugger.memory.component.MemoryViewManager; + +public class ShowClassesWithDiffAction extends ToggleAction { + @Override + public boolean isSelected(AnActionEvent e) { + return MemoryViewManager.getInstance().isNeedShowDiffOnly(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + Project project = e.getProject(); + if (project != null) { + MemoryViewManager.getInstance().setShowDiffOnly(state); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithInstanceAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithInstanceAction.java new file mode 100644 index 000000000000..17681f97dfb9 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowClassesWithInstanceAction.java @@ -0,0 +1,21 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.openapi.project.Project; +import org.jetbrains.debugger.memory.component.MemoryViewManager; + +public class ShowClassesWithInstanceAction extends ToggleAction { + @Override + public boolean isSelected(AnActionEvent e) { + return MemoryViewManager.getInstance().isNeedShowInstancesOnly(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + Project project = e.getProject(); + if (project != null) { + MemoryViewManager.getInstance().setShowWithInstancesOnly(state); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesAction.java new file mode 100644 index 000000000000..865e805caaf0 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesAction.java @@ -0,0 +1,17 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; + +abstract class ShowInstancesAction extends ClassesActionBase { + @Override + public void update(AnActionEvent e) { + boolean enabled = isEnabled(e); + if (enabled) { + e.getPresentation().setText(String.format("%s (%d)", getLabel(), getInstancesCount(e))); + } + } + + protected abstract String getLabel(); + + protected abstract int getInstancesCount(AnActionEvent e); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesByClassAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesByClassAction.java new file mode 100644 index 000000000000..f4ccf9271eea --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesByClassAction.java @@ -0,0 +1,38 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebuggerManager; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.memory.view.InstancesWindow; + +public class ShowInstancesByClassAction extends DebuggerTreeAction { + @Override + protected boolean isEnabled(@NotNull XValueNodeImpl node, @NotNull AnActionEvent e) { + ObjectReference ref = getObjectReference(node); + if(ref != null) { + String text = String.format("Show %s Objects...", StringUtil.getShortName(ref.referenceType().name())); + e.getPresentation().setText(text); + } + + return ref != null; + } + + @Override + protected void perform(XValueNodeImpl node, @NotNull String nodeName, AnActionEvent e) { + Project project = e.getProject(); + if (project != null) { + XDebugSession debugSession = XDebuggerManager.getInstance(project).getCurrentSession(); + ObjectReference ref = getObjectReference(node); + if(debugSession != null && ref != null){ + ReferenceType referenceType = ref.referenceType(); + new InstancesWindow(debugSession, referenceType::instances, referenceType.name()).show(); + } + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesFromClassesViewAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesFromClassesViewAction.java new file mode 100644 index 000000000000..040432aeb97a --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowInstancesFromClassesViewAction.java @@ -0,0 +1,40 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ReferenceType; +import org.jetbrains.debugger.memory.view.ClassesTable; +import org.jetbrains.debugger.memory.view.InstancesWindow; + +public class ShowInstancesFromClassesViewAction extends ShowInstancesAction { + private static final String POPUP_ELEMENT_LABEL = "Show Instances"; + @Override + protected boolean isEnabled(AnActionEvent e) { + return super.isEnabled(e) && getSelectedClass(e) != null; + } + + @Override + protected void perform(AnActionEvent e) { + XDebugSession debugSession = getDebugSession(e); + ReferenceType selectedClass = getSelectedClass(e); + if (debugSession != null && selectedClass != null) { + new InstancesWindow(debugSession, selectedClass::instances, selectedClass.name()).show(); + } + } + + @Override + protected String getLabel() { + return POPUP_ELEMENT_LABEL; + } + + @Override + protected int getInstancesCount(AnActionEvent e) { + ClassesTable.ReferenceCountProvider countProvider = e.getData(ClassesTable.REF_COUNT_PROVIDER_KEY); + ReferenceType selectedClass = getSelectedClass(e); + if (countProvider == null || selectedClass == null) { + return -1; + } + + return countProvider.getTotalCount(selectedClass); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowNewInstancesAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowNewInstancesAction.java new file mode 100644 index 000000000000..cfa3922f4089 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowNewInstancesAction.java @@ -0,0 +1,46 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ReferenceType; +import org.jetbrains.debugger.memory.utils.InstancesProvider; +import org.jetbrains.debugger.memory.view.ClassesTable; +import org.jetbrains.debugger.memory.view.InstancesWindow; + +public class ShowNewInstancesAction extends ShowInstancesAction { + private static final String POPUP_ELEMENT_LABEL = "Show New Instances"; + + @Override + protected boolean isEnabled(AnActionEvent e) { + XDebugSession session = getDebugSession(e); + ReferenceType selectedClass = getSelectedClass(e); + InstancesProvider provider = e.getData(ClassesTable.NEW_INSTANCES_PROVIDER_KEY); + return super.isEnabled(e) && session != null && selectedClass != null && provider != null; + } + + @Override + protected String getLabel() { + return POPUP_ELEMENT_LABEL; + } + + @Override + protected int getInstancesCount(AnActionEvent e) { + ClassesTable.ReferenceCountProvider countProvider = e.getData(ClassesTable.REF_COUNT_PROVIDER_KEY); + ReferenceType selectedClass = getSelectedClass(e); + if (countProvider == null || selectedClass == null) { + return -1; + } + + return countProvider.getNewInstancesCount(selectedClass); + } + + @Override + protected void perform(AnActionEvent e) { + XDebugSession session = getDebugSession(e); + ReferenceType selectedClass = getSelectedClass(e); + InstancesProvider provider = e.getData(ClassesTable.NEW_INSTANCES_PROVIDER_KEY); + if (selectedClass != null && provider != null && session != null) { + new InstancesWindow(session, provider, selectedClass.name()).show(); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowTrackedAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowTrackedAction.java new file mode 100644 index 000000000000..9c8228530b45 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/ShowTrackedAction.java @@ -0,0 +1,19 @@ +package org.jetbrains.debugger.memory.action; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.ToggleAction; +import org.jetbrains.debugger.memory.component.MemoryViewManager; + +public class ShowTrackedAction extends ToggleAction { + @Override + public boolean isSelected(AnActionEvent e) { + return MemoryViewManager.getInstance().isNeedShowTrackedOnly(); + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + if (e.getProject() != null) { + MemoryViewManager.getInstance().setShowTrackedOnly(state); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/JumpToAllocationSourceAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/JumpToAllocationSourceAction.java new file mode 100644 index 000000000000..218fdfa6084e --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/JumpToAllocationSourceAction.java @@ -0,0 +1,54 @@ +package org.jetbrains.debugger.memory.action.tracking; + +import com.intellij.debugger.DebuggerManager; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebuggerManager; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.action.DebuggerTreeAction; +import org.jetbrains.debugger.memory.component.CreationPositionTracker; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; +import org.jetbrains.debugger.memory.view.InstancesTree; +import org.jetbrains.debugger.memory.view.StackFramePopup; + +import java.util.List; + +public class JumpToAllocationSourceAction extends DebuggerTreeAction { + @Override + public void update(AnActionEvent e) { + e.getPresentation().setVisible(getStack(e) != null); + } + + @Override + protected void perform(XValueNodeImpl node, @NotNull String nodeName, AnActionEvent e) { + Project project = e.getProject(); + List stack = getStack(e); + if(project != null && stack != null) { + XDebugSession session = XDebuggerManager.getInstance(project).getCurrentSession(); + if(session != null) { + GlobalSearchScope searchScope = DebuggerManager.getInstance(project) + .getDebugProcess(session.getDebugProcess().getProcessHandler()).getSearchScope(); + new StackFramePopup(project, stack, searchScope).show(); + } + } + } + + @Nullable + private List getStack(AnActionEvent e) { + Project project = e.getProject(); + XValueNodeImpl selectedNode = getSelectedNode(e.getDataContext()); + ObjectReference ref = selectedNode != null ? getObjectReference(selectedNode) : null; + if(project == null || ref == null) { + return null; + } + + XDebugSession session = e.getData(InstancesTree.DEBUG_SESSION_DATA_KEY); + CreationPositionTracker tracker = CreationPositionTracker.getInstance(project); + return session == null || tracker == null ? null : tracker.getStack(session, ref); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/TrackInstancesToggleAction.java b/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/TrackInstancesToggleAction.java new file mode 100644 index 000000000000..2a35327966df --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/action/tracking/TrackInstancesToggleAction.java @@ -0,0 +1,64 @@ +package org.jetbrains.debugger.memory.action.tracking; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ArrayType; +import com.sun.jdi.ReferenceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.component.InstancesTracker; +import org.jetbrains.debugger.memory.tracking.TrackingType; +import org.jetbrains.debugger.memory.view.ClassesTable; + +public class TrackInstancesToggleAction extends ToggleAction { + @Override + public void update(@NotNull AnActionEvent e) { + ReferenceType selectedClass = getSelectedClass(e); + if (selectedClass instanceof ArrayType) { + e.getPresentation().setEnabled(false); + } else { + super.update(e); + } + } + + @Override + public boolean isSelected(AnActionEvent e) { + ReferenceType selectedClass = getSelectedClass(e); + XDebugSession debugSession = getDebugSession(e); + if (debugSession != null && selectedClass != null) { + InstancesTracker tracker = InstancesTracker.getInstance(debugSession.getProject()); + return tracker.isTracked(selectedClass.name()); + } + + return false; + } + + @Override + public void setSelected(AnActionEvent e, boolean state) { + ReferenceType selectedClass = getSelectedClass(e); + XDebugSession debugSession = getDebugSession(e); + if (selectedClass != null && debugSession != null) { + InstancesTracker tracker = InstancesTracker.getInstance(debugSession.getProject()); + boolean isAlreadyTracked = tracker.isTracked(selectedClass.name()); + + if (isAlreadyTracked && !state) { + tracker.remove(selectedClass.name()); + } + + if (!isAlreadyTracked && state) { + tracker.add(selectedClass.name(), TrackingType.CREATION); + } + } + } + + @Nullable + private ReferenceType getSelectedClass(AnActionEvent e) { + return e.getData(ClassesTable.SELECTED_CLASS_KEY); + } + + @Nullable + private XDebugSession getDebugSession(AnActionEvent e) { + return e.getData(ClassesTable.DEBUG_SESSION_KEY); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/component/CreationPositionTracker.java b/java/debugger/impl/src/com/intellij/debugger/memory/component/CreationPositionTracker.java new file mode 100644 index 000000000000..ee54aab35e3e --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/component/CreationPositionTracker.java @@ -0,0 +1,102 @@ +package org.jetbrains.debugger.memory.component; + +import com.intellij.openapi.components.AbstractProjectComponent; +import com.intellij.openapi.project.Project; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebugSessionListener; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class CreationPositionTracker extends AbstractProjectComponent { + /** + * Stores all tracked instance for each debug session. + */ + private final ConcurrentHashMap>> + mySession2Reference2Stack = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap>> + myPinnedSession2Reference2Stack = new ConcurrentHashMap<>(); + + public CreationPositionTracker(Project project) { + super(project); + } + + @Nullable + public static CreationPositionTracker getInstance(@NotNull Project project) { + return project.isDisposed() ? null : project.getComponent(CreationPositionTracker.class); + } + + @Nullable + public List getStack(@NotNull XDebugSession session, @NotNull ObjectReference ref) { + List stack = extract(mySession2Reference2Stack, session, ref); + return stack != null ? stack : extract(myPinnedSession2Reference2Stack, session, ref); + } + + public void addStack(@NotNull XDebugSession session, @NotNull ObjectReference ref, + @NotNull List< StackFrameDescriptor> stack) { + if (!mySession2Reference2Stack.containsKey(session)) { + mySession2Reference2Stack.put(session, new ConcurrentHashMap<>()); + session.addSessionListener(new XDebugSessionListener() { + @Override + public void sessionStopped() { + mySession2Reference2Stack.remove(session); + } + }); + } + + mySession2Reference2Stack.get(session).put(ref, stack); + } + + public void releaseBySession(@NotNull XDebugSession session) { + if(mySession2Reference2Stack.containsKey(session)) { + mySession2Reference2Stack.put(session, new ConcurrentHashMap<>()); + } + } + + public void unpinStacks(@NotNull XDebugSession session, @NotNull ReferenceType ref) { + Map> ref2Stack = myPinnedSession2Reference2Stack.getOrDefault(session, null); + if (ref2Stack != null) { + Iterator iterator = ref2Stack.keySet().iterator(); + while (iterator.hasNext()) { + ObjectReference reference = iterator.next(); + if (ref.equals(reference.referenceType())) { + iterator.remove(); + } + } + + if (ref2Stack.isEmpty()) { + myPinnedSession2Reference2Stack.remove(session); + } + } + } + + public void pinStacks(@NotNull XDebugSession session, @NotNull ReferenceType ref) { + Map> ref2Stack = mySession2Reference2Stack.getOrDefault(session, null); + if (ref2Stack != null) { + Map> ref2StacksByReferenceType = ref2Stack.entrySet().stream() + .filter(entry -> ref.equals(entry.getKey().referenceType())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + myPinnedSession2Reference2Stack.put(session, ref2StacksByReferenceType); + } + } + + @Nullable + private static T extract(@NotNull Map> map, + @NotNull XDebugSession session, @NotNull ObjectReference ref) { + Map ref2something = map.getOrDefault(session, null); + if(ref2something != null) { + return ref2something.getOrDefault(ref, null); + } + + return null; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/component/InstancesTracker.java b/java/debugger/impl/src/com/intellij/debugger/memory/component/InstancesTracker.java new file mode 100644 index 000000000000..1a47159e20ca --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/component/InstancesTracker.java @@ -0,0 +1,109 @@ +package org.jetbrains.debugger.memory.component; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.components.*; +import com.intellij.openapi.project.Project; +import com.intellij.util.EventDispatcher; +import com.intellij.util.containers.HashMap; +import com.intellij.util.xmlb.annotations.AbstractCollection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.event.InstancesTrackerListener; +import org.jetbrains.debugger.memory.tracking.TrackingType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@State(name = "InstancesTracker", storages = @Storage(StoragePathMacros.WORKSPACE_FILE)) +public class InstancesTracker extends AbstractProjectComponent + implements PersistentStateComponent { + private final EventDispatcher myDispatcher = + EventDispatcher.create(InstancesTrackerListener.class); + private MyState myState = new MyState(); + + public InstancesTracker(Project project) { + super(project); + } + + public static InstancesTracker getInstance(@NotNull Project project) { + return project.getComponent(InstancesTracker.class); + } + + public boolean isTracked(@NotNull String className) { + return myState.classes.containsKey(className); + } + + public boolean isBackgroundTrackingEnabled() { + return myState.isBackgroundTrackingEnabled; + } + + @Nullable + public TrackingType getTrackingType(@NotNull String className) { + return myState.classes.getOrDefault(className, null); + } + + @NotNull + public Map getTrackedClasses() { + return new HashMap<>(myState.classes); + } + + public void add(@NotNull String name, @NotNull TrackingType type) { + if (type.equals(myState.classes.getOrDefault(name, null))) { + return; + } + + myState.classes.put(name, type); + myDispatcher.getMulticaster().classChanged(name, type); + } + + public boolean remove(@NotNull String name) { + TrackingType removed = myState.classes.remove(name); + if (removed != null) { + myDispatcher.getMulticaster().classRemoved(name); + return true; + } + + return false; + } + + public void addTrackerListener(@NotNull InstancesTrackerListener listener, @NotNull Disposable parentDisposable) { + myDispatcher.addListener(listener, parentDisposable); + } + + public void setBackgroundTackingEnabled(boolean state) { + boolean oldState = myState.isBackgroundTrackingEnabled; + if(state != oldState) { + myState.isBackgroundTrackingEnabled = state; + myDispatcher.getMulticaster().backgroundTrackingValueChanged(state); + } + } + + @Nullable + @Override + public MyState getState() { + return new MyState(myState); + } + + @Override + public void loadState(MyState state) { + myState = new MyState(state); + } + + static class MyState { + boolean isBackgroundTrackingEnabled = false; + + @NotNull + @AbstractCollection(surroundWithTag = false, elementTypes = {Map.Entry.class}) + final Map classes = new ConcurrentHashMap<>(); + + MyState() { + } + + MyState(@NotNull MyState state) { + isBackgroundTrackingEnabled = state.isBackgroundTrackingEnabled; + for (Map.Entry classState : state.classes.entrySet()) { + classes.put(classState.getKey(), classState.getValue()); + } + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManager.java b/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManager.java new file mode 100644 index 000000000000..58d42f890fd6 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManager.java @@ -0,0 +1,87 @@ +package org.jetbrains.debugger.memory.component; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.util.EventDispatcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.event.MemoryViewManagerListener; +import org.jetbrains.debugger.memory.toolwindow.MemoryViewToolWindowFactory; + +@State(name = "MemoryViewSettings", storages = @Storage("memory.view.xml")) +public class MemoryViewManager extends ApplicationComponent.Adapter + implements PersistentStateComponent { + private final EventDispatcher myDispatcher = + EventDispatcher.create(MemoryViewManagerListener.class); + private MemoryViewManagerState myState = new MemoryViewManagerState(); + + public static MemoryViewManager getInstance() { + return ApplicationManager.getApplication().getComponent(MemoryViewManager.class); + } + + @NotNull + @Override + public MemoryViewManagerState getState() { + return new MemoryViewManagerState(myState); + } + + @Override + public void loadState(MemoryViewManagerState state) { + if (state == null) { + state = new MemoryViewManagerState(); + } + + myState = state; + fireStateChanged(); + } + + public void setShowDiffOnly(boolean value) { + if (myState.isShowWithDiffOnly != value) { + myState.isShowWithDiffOnly = value; + fireStateChanged(); + } + } + + public void setShowWithInstancesOnly(boolean value) { + if (myState.isShowWithInstancesOnly != value) { + myState.isShowWithInstancesOnly = value; + fireStateChanged(); + } + } + + public void setShowTrackedOnly(boolean value) { + if (myState.isShowTrackedOnly != value) { + myState.isShowTrackedOnly = value; + fireStateChanged(); + } + } + + public boolean isNeedShowDiffOnly() { + return myState.isShowWithDiffOnly; + } + + public boolean isNeedShowInstancesOnly() { + return myState.isShowWithInstancesOnly; + } + + public boolean isNeedShowTrackedOnly() { + return myState.isShowTrackedOnly; + } + + public void addMemoryViewManagerListener(MemoryViewManagerListener listener, @NotNull Disposable parentDisposable) { + myDispatcher.addListener(listener, parentDisposable); + } + + @Nullable + public ToolWindow getToolWindow(Project project) { + return ToolWindowManager.getInstance(project).getToolWindow(MemoryViewToolWindowFactory.TOOL_WINDOW_ID); + } + + private void fireStateChanged() { + myDispatcher.getMulticaster().stateChanged(new MemoryViewManagerState(myState)); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManagerState.java b/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManagerState.java new file mode 100644 index 000000000000..07e6f7272c45 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/component/MemoryViewManagerState.java @@ -0,0 +1,16 @@ +package org.jetbrains.debugger.memory.component; + +public final class MemoryViewManagerState { + public boolean isShowWithInstancesOnly = true; + public boolean isShowWithDiffOnly = false; + public boolean isShowTrackedOnly = false; + + MemoryViewManagerState() { + } + + MemoryViewManagerState(MemoryViewManagerState other) { + isShowWithInstancesOnly = other.isShowWithInstancesOnly; + isShowWithDiffOnly = other.isShowWithDiffOnly; + isShowTrackedOnly = other.isShowTrackedOnly; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/event/InstancesTrackerListener.java b/java/debugger/impl/src/com/intellij/debugger/memory/event/InstancesTrackerListener.java new file mode 100644 index 000000000000..e01372032a1c --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/event/InstancesTrackerListener.java @@ -0,0 +1,17 @@ +package org.jetbrains.debugger.memory.event; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.memory.tracking.TrackingType; + +import java.util.EventListener; + +public interface InstancesTrackerListener extends EventListener { + default void classChanged(@NotNull String name, @NotNull TrackingType type) { + } + + default void classRemoved(@NotNull String name) { + } + + default void backgroundTrackingValueChanged(boolean newState) { + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/event/MemoryViewManagerListener.java b/java/debugger/impl/src/com/intellij/debugger/memory/event/MemoryViewManagerListener.java new file mode 100644 index 000000000000..1c7e1cf8f031 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/event/MemoryViewManagerListener.java @@ -0,0 +1,11 @@ +package org.jetbrains.debugger.memory.event; + +import org.jetbrains.debugger.memory.component.MemoryViewManagerState; +import org.jetbrains.annotations.NotNull; + +import java.util.EventListener; + +@FunctionalInterface +public interface MemoryViewManagerListener extends EventListener { + void stateChanged(@NotNull MemoryViewManagerState state); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/RetainedSizeFetcher.java b/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/RetainedSizeFetcher.java new file mode 100644 index 000000000000..78a9f66079ba --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/RetainedSizeFetcher.java @@ -0,0 +1,109 @@ +package org.jetbrains.debugger.memory.sizeof; + +import com.sun.jdi.*; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +public class RetainedSizeFetcher extends SizeOfEvalStrategy { + private final static long REFERENCE_SIZE = 4; + private final static long CHAR_SIZE = 2; + private final static long INT_SIZE = 4; + private final static long FLOAT_SIZE = 4; + private final static long DOUBLE_SIZE = 8; + private final static long BYTE_SIZE = 1; + private final static long BOOLEAN_SIZE = 1; // ? + private final static long LONG_SIZE = 8; + private final static long SHORT_SIZE = 2; + + private final boolean myCheckReferringObjects; + + public RetainedSizeFetcher(boolean checkReferringObjects) { + myCheckReferringObjects = checkReferringObjects; + } + + private static long getPrimitiveTypeSize(PrimitiveValue value) { + if (value instanceof IntegerValue) { + return INT_SIZE; + } + if (value instanceof LongValue) { + return LONG_SIZE; + } + if (value instanceof DoubleValue) { + return DOUBLE_SIZE; + } + if (value instanceof FloatValue) { + return FLOAT_SIZE; + } + if (value instanceof CharValue) { + return CHAR_SIZE; + } + if (value instanceof ByteValue) { + return BYTE_SIZE; + } + if (value instanceof BooleanValue) { + return BOOLEAN_SIZE; + } + + return SHORT_SIZE; + } + + private long getValueSize(Value value, Collection visited) { + if (value == null) { + return 0; + } + + if (value instanceof PrimitiveValue) { + return getPrimitiveTypeSize((PrimitiveValue) value); + } else { + ObjectReference ref = (ObjectReference) value; + if (visited.contains(ref)) { + return 0; + } + + if (myCheckReferringObjects) { + long startTime = System.nanoTime(); + int needReferringObjectCount = visited.size() + 1; + Collection referringObjects = ref.referringObjects(needReferringObjectCount); + long duration = System.nanoTime() - startTime; + System.out.println("referringObjects(" + needReferringObjectCount + ") duration = " + duration + "ns "+ + "(" + TimeUnit.NANOSECONDS.toMillis(duration) + " ms)."); + if (referringObjects.stream().anyMatch(objectReference -> !visited.contains(objectReference))) { + return 0; + } + } + + return sizeOfImpl(ref, visited, false); + } + } + + @Override + protected long sizeOfImpl(ObjectReference ref, Collection visited, boolean isRoot) { + visited.add(ref); + long result = 0; + + if (ref instanceof ArrayReference) { + ArrayReference arrRef = (ArrayReference) ref; + int length = arrRef.length(); + for (int i = 0; i < length; i++) { + result += getValueSize(arrRef.getValue(i), visited); + } + } + + for (Field field : ref.referenceType().allFields()) { + Value val = ref.getValue(field); + if (field.isStatic()) { + continue; + } + + result += REFERENCE_SIZE; + if (val == null) { + continue; + } + + result += getValueSize(val, visited); + } + + return result; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/SizeOfEvalStrategy.java b/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/SizeOfEvalStrategy.java new file mode 100644 index 000000000000..962909f3fbaf --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/sizeof/SizeOfEvalStrategy.java @@ -0,0 +1,21 @@ +package org.jetbrains.debugger.memory.sizeof; + +import com.sun.jdi.ObjectReference; + +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; + +abstract class SizeOfEvalStrategy { + public long sizeOf(ObjectReference ref) { + long startTime = System.nanoTime(); + long result = sizeOfImpl(ref, new HashSet<>(), true); + long duration = System.nanoTime() - startTime; + System.out.printf("org.jetbrains.plugin.sizeof [%d] == %d. time = %d ns (%d ms).%n", + ref.uniqueID(), result, duration, TimeUnit.NANOSECONDS.toMillis(duration)); + + return result; + } + + protected abstract long sizeOfImpl(ObjectReference ref, Collection visited, boolean isRoot); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/toolwindow/MemoryViewToolWindowFactory.java b/java/debugger/impl/src/com/intellij/debugger/memory/toolwindow/MemoryViewToolWindowFactory.java new file mode 100644 index 000000000000..d844cf1bf9c3 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/toolwindow/MemoryViewToolWindowFactory.java @@ -0,0 +1,216 @@ +package org.jetbrains.debugger.memory.toolwindow; + +import com.intellij.execution.Executor; +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.execution.ui.RunContentManager; +import com.intellij.execution.ui.RunContentWithExecutorListener; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.Separator; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ex.ToolWindowEx; +import com.intellij.openapi.wm.ex.ToolWindowManagerAdapter; +import com.intellij.openapi.wm.impl.ToolWindowImpl; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.xdebugger.XDebugProcess; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebuggerManager; +import com.intellij.xdebugger.XDebuggerManagerListener; +import icons.MemoryViewIcons; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.component.MemoryViewManager; +import org.jetbrains.debugger.memory.view.ClassesFilteredView; + +import javax.swing.*; +import java.awt.*; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryViewToolWindowFactory implements ToolWindowFactory, DumbAware { + private static final Logger LOG = Logger.getInstance(ClassesFilteredView.class); + public final static String TOOL_WINDOW_ID = "Memory View"; + + private final JComponent myEmptyContent; + private final JComponent myMemoryViewNotSupportedContent; + + private final Map myMemoryViews = new ConcurrentHashMap<>(); + + @Nullable + private ClassesFilteredView myCurrentView = null; + + { + myEmptyContent = new JBLabel("Run debugging to see loaded classes", SwingConstants.CENTER); + myMemoryViewNotSupportedContent = new JBLabel("The Memory View not available for this session", + SwingConstants.CENTER); + } + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + MessageBusConnection connection = project.getMessageBus().connect(project); + connection.subscribe(XDebuggerManager.TOPIC, new MyDebuggerStatusChangedListener()); + connection.subscribe(RunContentManager.TOPIC, new RunContentWithExecutorListener() { + @Override + public void contentSelected(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor) { + SwingUtilities.invokeLater(() -> updateCurrentMemoryView(project, toolWindow)); + } + + @Override + public void contentRemoved(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor) { + SwingUtilities.invokeLater(() -> updateCurrentMemoryView(project, toolWindow)); + } + }); + + ((ToolWindowImpl) toolWindow).getToolWindowManager() + .addToolWindowManagerListener(new ToolWindowManagerAdapter() { + @Override + public void stateChanged() { + if (!myMemoryViews.isEmpty() && !toolWindow.isDisposed()) { + myMemoryViews.values().forEach(classesFilteredView -> + classesFilteredView.setActive(toolWindow.isVisible())); + } + } + }, project); + + ActionGroup group = new DefaultActionGroup( + ActionManager.getInstance().getAction("MemoryView.ShowOnlyWithInstances"), + ActionManager.getInstance().getAction("MemoryView.ShowOnlyWithDiff"), + ActionManager.getInstance().getAction("MemoryView.ShowOnlyTracked"), + Separator.getInstance(), + ActionManager.getInstance().getAction("MemoryView.EnableTrackingWithClosedWindow") + ); + ((ToolWindowEx) toolWindow).setAdditionalGearActions(group); + toolWindow.getComponent().setLayout(new BorderLayout()); + + for (XDebugSession session : XDebuggerManager.getInstance(project).getDebugSessions()) { + addNewSession(session); + } + + updateCurrentMemoryView(project, toolWindow); + } + + private void addNewSession(@NotNull XDebugSession session) { + LOG.assertTrue(SwingUtilities.isEventDispatchThread()); + ToolWindow toolWindow = getToolWindow(session.getProject()); + if (!myMemoryViews.containsKey(session)) { + try { + ClassesFilteredView newView = new ClassesFilteredView(session); + newView.setActive(toolWindow != null && toolWindow.isVisible()); + myMemoryViews.put(session, newView); + } catch (Throwable e) { + LOG.warn("Cannot create new instance of the memory view. " + e.getMessage()); + } + } + } + + private void removeSession(@NotNull XDebugSession session) { + ClassesFilteredView removed = myMemoryViews.remove(session); + if (removed != null) { + Disposer.dispose(removed); + } + } + + private void updateCurrentMemoryView(@NotNull Project project, @NotNull ToolWindow toolWindow) { + if (!project.isDisposed()) { + XDebugSession session = XDebuggerManager.getInstance(project).getCurrentSession(); + if (session != null) { + ClassesFilteredView view; + if (myMemoryViews.containsKey(session)) { + view = myMemoryViews.get(session); + replaceToolWindowContent(toolWindow, view); + } else { + view = null; + replaceToolWindowContent(toolWindow, myMemoryViewNotSupportedContent); + } + + if (myCurrentView != null) { + myCurrentView.setActive(false); + } + + myCurrentView = view; + return; + } + } + + replaceToolWindowContent(toolWindow, myEmptyContent); + } + + private void replaceToolWindowContent(@NotNull ToolWindow toolWindow, JComponent comp) { + LOG.assertTrue(SwingUtilities.isEventDispatchThread()); + JComponent toolWindowComp = toolWindow.getComponent(); + toolWindowComp.removeAll(); + toolWindowComp.add(comp); + toolWindowComp.repaint(); + } + + @Nullable + private ToolWindow getToolWindow(@NotNull Project project) { + return MemoryViewManager.getInstance().getToolWindow(project); + } + + public static class Condition implements com.intellij.openapi.util.Condition { + @Override + public boolean value(Project project) { + MessageBusConnection connection = project.getMessageBus().connect(project); + connection.subscribe(XDebuggerManager.TOPIC, new XDebuggerManagerListener() { + @Override + public void processStarted(@NotNull XDebugProcess debugProcess) { + updateIcon(debugProcess.getSession().getProject(), true); + } + + @Override + public void processStopped(@NotNull XDebugProcess debugProcess) { + Project project = debugProcess.getSession().getProject(); + boolean enabled = Arrays.stream(XDebuggerManager.getInstance(project) + .getDebugSessions()).anyMatch(session -> !session.getDebugProcess().equals(debugProcess)); + updateIcon(project, enabled); + } + + private void updateIcon(@NotNull Project project, boolean enabled) { + ToolWindow toolWindow = MemoryViewManager.getInstance().getToolWindow(project); + if (toolWindow != null) { + SwingUtilities.invokeLater(() -> + toolWindow.setIcon(enabled ? MemoryViewIcons.MAIN_ENABLED : MemoryViewIcons.MAIN_DISABLED)); + } + } + }); + return true; + } + } + + private final class MyDebuggerStatusChangedListener implements XDebuggerManagerListener { + @Override + public void processStarted(@NotNull XDebugProcess xDebugProcess) { + SwingUtilities.invokeLater(() -> { + XDebugSession session = xDebugProcess.getSession(); + addNewSession(session); + updateView(session); + }); + } + + @Override + public void processStopped(@NotNull XDebugProcess xDebugProcess) { + XDebugSession session = xDebugProcess.getSession(); + removeSession(session); + SwingUtilities.invokeLater(() -> updateView(session)); + } + + private void updateView(@NotNull XDebugSession debugSession) { + Project project = debugSession.getProject(); + if (!project.isDisposed()) { + ToolWindow toolWindow = getToolWindow(project); + if (toolWindow != null) { + updateCurrentMemoryView(project, toolWindow); + } + } + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/BackgroundTracker.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/BackgroundTracker.java new file mode 100644 index 000000000000..51ce3955b786 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/BackgroundTracker.java @@ -0,0 +1,5 @@ +package org.jetbrains.debugger.memory.tracking; + +interface BackgroundTracker { + void setBackgroundMode(boolean isBackgroundMode); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ClassPreparedListener.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ClassPreparedListener.java new file mode 100644 index 000000000000..f102d4905e30 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ClassPreparedListener.java @@ -0,0 +1,44 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.DebuggerManager; +import com.intellij.debugger.engine.DebugProcess; +import com.intellij.debugger.engine.DebugProcessImpl; +import com.intellij.debugger.requests.ClassPrepareRequestor; +import com.intellij.openapi.project.Project; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.request.ClassPrepareRequest; +import org.jetbrains.annotations.NotNull; + +public abstract class ClassPreparedListener { + private final XDebugSession myDebugSession; + private final Project myProject; + + protected ClassPreparedListener(@NotNull String className, + @NotNull XDebugSession debugSession) { + myProject = debugSession.getProject(); + myDebugSession = debugSession; + DebugProcessImpl debugProcess = getDebugProcess(); + ClassPrepareRequestor request = new MyClassPreparedRequest(); + ClassPrepareRequest classPrepareRequest = debugProcess.getRequestsManager() + .createClassPrepareRequest(request, className); + if (classPrepareRequest != null) { + classPrepareRequest.enable(); + } + } + + public abstract void onClassPrepared(@NotNull ReferenceType referenceType); + + private DebugProcessImpl getDebugProcess() { + return (DebugProcessImpl) DebuggerManager.getInstance(myProject) + .getDebugProcess(myDebugSession.getDebugProcess().getProcessHandler()); + } + + private final class MyClassPreparedRequest implements ClassPrepareRequestor { + @Override + public void processClassPrepare(DebugProcess debuggerProcess, ReferenceType referenceType) { + getDebugProcess().getRequestsManager().deleteRequest(this); + onClassPrepared(referenceType); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ConstructorInstancesTracker.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ConstructorInstancesTracker.java new file mode 100644 index 000000000000..44ebf43d10b8 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/ConstructorInstancesTracker.java @@ -0,0 +1,259 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.DebuggerManager; +import com.intellij.debugger.engine.DebugProcessImpl; +import com.intellij.debugger.engine.SuspendContextImpl; +import com.intellij.debugger.engine.evaluation.EvaluateException; +import com.intellij.debugger.engine.events.DebuggerCommandImpl; +import com.intellij.debugger.engine.events.SuspendContextCommandImpl; +import com.intellij.debugger.jdi.StackFrameProxyImpl; +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl; +import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType; +import com.intellij.debugger.ui.breakpoints.LineBreakpoint; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.breakpoints.XBreakpoint; +import com.intellij.xdebugger.impl.XDebuggerManagerImpl; +import com.intellij.xdebugger.impl.breakpoints.LineBreakpointState; +import com.intellij.xdebugger.impl.breakpoints.XLineBreakpointImpl; +import com.sun.jdi.Location; +import com.sun.jdi.Method; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.event.LocatableEvent; +import com.sun.jdi.request.BreakpointRequest; +import com.sun.jdi.request.EventRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.component.CreationPositionTracker; +import org.jetbrains.debugger.memory.component.InstancesTracker; +import org.jetbrains.debugger.memory.event.InstancesTrackerListener; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; +import org.jetbrains.java.debugger.breakpoints.properties.JavaLineBreakpointProperties; + +import java.util.*; +import java.util.stream.Collectors; + +public class ConstructorInstancesTracker implements TrackerForNewInstances, Disposable, BackgroundTracker { + private static final int TRACKED_INSTANCES_LIMIT = 2000; + private final ReferenceType myReference; + private final XDebugSession myDebugSession; + private final CreationPositionTracker myPositionTracker; + private final DebugProcessImpl myDebugProcess; + private final MyConstructorBreakpoints myBreakpoint; + + @Nullable + private HashSet myNewObjects = null; + + @NotNull + private HashSet myTrackedObjects = new HashSet<>(); + + private volatile boolean myIsBackgroundMode; + private volatile boolean myIsBackgroundTrackingEnabled; + + public ConstructorInstancesTracker(@NotNull ReferenceType ref, + @NotNull XDebugSession debugSession) { + myReference = ref; + myDebugSession = debugSession; + myPositionTracker = CreationPositionTracker.getInstance(debugSession.getProject()); + Project project = debugSession.getProject(); + myIsBackgroundTrackingEnabled = InstancesTracker.getInstance(myDebugSession.getProject()) + .isBackgroundTrackingEnabled(); + + InstancesTracker.getInstance(myDebugSession.getProject()).addTrackerListener(new InstancesTrackerListener() { + @Override + public void backgroundTrackingValueChanged(boolean newState) { + if (myIsBackgroundTrackingEnabled != newState) { + myIsBackgroundTrackingEnabled = newState; + myDebugProcess.getManagerThread().schedule(new DebuggerCommandImpl() { + @Override + protected void action() throws Exception { + if (newState) { + myBreakpoint.enable(); + } else { + myBreakpoint.disable(); + } + } + }); + } + } + }, this); + + myDebugProcess = (DebugProcessImpl) DebuggerManager.getInstance(project) + .getDebugProcess(debugSession.getDebugProcess().getProcessHandler()); + JavaLineBreakpointType breakPointType = new JavaLineBreakpointType(); + + XBreakpoint bpn = new XLineBreakpointImpl<>(breakPointType, + ((XDebuggerManagerImpl) XDebuggerManagerImpl.getInstance(project)).getBreakpointManager(), + new JavaLineBreakpointProperties(), + new LineBreakpointState<>()); + + myBreakpoint = new MyConstructorBreakpoints(project, bpn); + myBreakpoint.createRequestForPreparedClass(myDebugProcess, myReference); + } + + public void obsolete() { + if (myNewObjects != null) { + myNewObjects.forEach(ObjectReference::enableCollection); + } + + myNewObjects = null; + if (!myIsBackgroundMode || myIsBackgroundTrackingEnabled) { + myBreakpoint.enable(); + } + myPositionTracker.releaseBySession(myDebugSession); + } + + public void commitTracked() { + myNewObjects = myTrackedObjects; + myTrackedObjects = new HashSet<>(); + } + + @NotNull + @Override + public List getNewInstances() { + return myNewObjects == null ? Collections.EMPTY_LIST : new ArrayList<>(myNewObjects); + } + + @Override + public int getCount() { + return myNewObjects == null ? 0 : myNewObjects.size(); + } + + public void enable() { + myBreakpoint.enable(); + } + + public void disable() { + myBreakpoint.disable(); + } + + @Override + public boolean isReady() { + return myNewObjects != null; + } + + @Override + public void dispose() { + myBreakpoint.dispose(); + } + + @Override + public void setBackgroundMode(boolean isBackgroundMode) { + if (myIsBackgroundMode == isBackgroundMode) { + return; + } + + myIsBackgroundMode = isBackgroundMode; + if (isBackgroundMode) { + doEnableBackgroundMode(); + } else { + doDisableBackgroundMode(); + } + } + + private void doDisableBackgroundMode() { + myBreakpoint.enable(); + } + + private void doEnableBackgroundMode() { + if (!myIsBackgroundTrackingEnabled) { + myBreakpoint.disable(); + } + } + + private final class MyConstructorBreakpoints extends LineBreakpoint + implements Disposable { + + private boolean myIsEnabled = false; + private final List myRequests = new ArrayList<>(); + private volatile boolean myIsDeleted = false; + + MyConstructorBreakpoints(Project project, XBreakpoint xBreakpoint) { + super(project, xBreakpoint); + } + + @Override + protected void createRequestForPreparedClass(DebugProcessImpl debugProcess, ReferenceType classType) { + classType.methods().stream().filter(Method::isConstructor).forEach(cons -> { + Location loc = cons.location(); + BreakpointRequest breakpointRequest = debugProcess.getRequestsManager().createBreakpointRequest(this, loc); + myRequests.add(breakpointRequest); + }); + + if (!myIsBackgroundMode || myIsBackgroundTrackingEnabled) { + enable(); + } + } + + @Override + public void reload() { + } + + @Override + public void dispose() { + myDebugProcess.getManagerThread().schedule(new DebuggerCommandImpl() { + @Override + protected void action() throws Exception { + disable(); + myDebugProcess.getRequestsManager().deleteRequest(MyConstructorBreakpoints.this); + myIsDeleted = true; + } + }); + } + + @Override + public boolean processLocatableEvent(SuspendContextCommandImpl action, LocatableEvent event) + throws EventProcessingException { + try { + SuspendContextImpl suspendContext = action.getSuspendContext(); + if (suspendContext != null) { + ObjectReference thisRef = getThisObject(suspendContext, event); + if (myReference.equals(thisRef.referenceType()) && myPositionTracker != null) { + thisRef.disableCollection(); + myTrackedObjects.add(thisRef); + ThreadReferenceProxyImpl threadReferenceProxy = suspendContext.getThread(); + List stack = threadReferenceProxy == null ? null : threadReferenceProxy.frames(); + + if (stack != null) { + List stackFrameDescriptors = stack.stream().map(frame -> { + try { + Location loc = frame.location(); + String typeName = loc.declaringType().name(); + String methodName = loc.method().name(); + return new StackFrameDescriptor(typeName, methodName, loc.lineNumber()); + } catch (EvaluateException e) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + myPositionTracker.addStack(myDebugSession, thisRef, stackFrameDescriptors); + } + } + } + } catch (EvaluateException e) { + return false; + } + + if (myTrackedObjects.size() >= TRACKED_INSTANCES_LIMIT) { + disable(); + } + + return false; + } + + void enable() { + if (!myIsEnabled && !myIsDeleted) { + myRequests.forEach(EventRequest::enable); + myIsEnabled = true; + } + } + + void disable() { + if (myIsEnabled && !myIsDeleted) { + myRequests.forEach(EventRequest::disable); + myIsEnabled = false; + } + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/HashBasedTracking.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/HashBasedTracking.java new file mode 100644 index 000000000000..110137d11191 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/HashBasedTracking.java @@ -0,0 +1,169 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.engine.SuspendContextImpl; +import com.intellij.debugger.engine.jdi.ThreadReferenceProxy; +import com.intellij.openapi.diagnostic.Logger; +import com.sun.jdi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +class HashBasedTracking extends InstanceTrackingStrategy { + private static final Logger LOG = Logger.getInstance(HashBasedTracking.class); + + private final ReferenceType myClassType; + + private MyHashCodeMethodWrapper myHashCodeWrapper; + private Set myHashesSet; + private MyState myState = MyState.WAIT_HASHES; + private int myCount = 0; + HashBasedTracking(@Nullable SuspendContextImpl suspendContext, + @NotNull ReferenceType classType, + @NotNull List initialInstances) { + myClassType = classType; + if (suspendContext != null) { + myHashesSet = toSetOfHashes(evalHashCodes(suspendContext, initialInstances)); + myState = myState.next(); + } + } + + @NotNull + @Override + protected List updateImpl(@NotNull SuspendContextImpl suspendContext, + @NotNull List references) { + Map> ref2hash = evalHashCodes(suspendContext, references); + List newInstances = new ArrayList<>(); + + for (ObjectReference ref : references) { + Optional hash = ref2hash.get(ref); + if (hash.isPresent() && !myState.equals(MyState.WAIT_HASHES) && !myHashesSet.contains(hash.get())) { + newInstances.add(ref); + } + } + + myHashesSet = toSetOfHashes(ref2hash); + myState = myState.next(); + myCount = newInstances.size(); + return newInstances; + } + + @Override + public int getCount() { + return myCount; + } + + @Override + public boolean isReady() { + return myState == MyState.READY; + } + + @NotNull + private Map> evalHashCodes(@NotNull SuspendContextImpl suspendContext, + @NotNull List references) { + MyHashCodeMethodWrapper hashCodeCaller = getHashCodeMethod(myClassType); + Map> result = new HashMap<>(); + ThreadReferenceProxy threadProxy = suspendContext.getThread(); + ThreadReference thread = threadProxy != null ? threadProxy.getThreadReference() : null; + if (thread == null || hashCodeCaller == null) { + LOG.warn(thread == null ? "Thread is null" : "hashCode not found"); + return result; + } + + long start = System.nanoTime(); + for (ObjectReference ref : references) { + Optional hash = Optional.empty(); + try { + if (!ref.isCollected()) { + ref.disableCollection(); + if (!ref.isCollected()) { + hash = hashCodeCaller.eval(ref, suspendContext, thread); + } + ref.enableCollection(); + } + } catch (InvalidTypeException | ClassNotLoadedException | + IncompatibleThreadStateException | InvocationException e) { + LOG.warn("Hash code evaluation failed", e); + } + + result.put(ref, hash); + } + + long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + LOG.info(String.format("Hash code evaluation: %d ms. Count = %d", duration, references.size())); + + return result; + } + + private Set toSetOfHashes(@NotNull Map> ref2hashCode) { + return ref2hashCode.values().stream() + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toCollection(HashSet::new)); + } + + @Nullable + private MyHashCodeMethodWrapper getHashCodeMethod(@NotNull ReferenceType referenceType) { + if (myHashCodeWrapper != null) { + return myHashCodeWrapper; + } + + final List referenceTypes = referenceType.virtualMachine().classesByName("java.util.Objects"); + if (!referenceTypes.isEmpty()) { + ClassType objectsClass = (ClassType) referenceTypes.get(0); + Method hashCodeFromObjects = objectsClass.methodsByName("hashCode").stream() + .filter(method -> method.argumentTypeNames().size() == 1 + && "java.lang.Object".equals(method.argumentTypeNames().get(0))) + .findFirst() + .orElse(null); + + myHashCodeWrapper = (ref, suspendContext, thread) -> { + final Value value = objectsClass.invokeMethod(thread, hashCodeFromObjects, + Collections.singletonList(ref), ClassType.INVOKE_SINGLE_THREADED); + return Optional.of(((IntegerValue) value).value()); + }; + } else { + final Method hashCode = referenceType.methodsByName("hashCode").stream() + .filter(method -> method.argumentTypeNames().isEmpty()) + .findFirst() + .orElse(null); + + myHashCodeWrapper = hashCode == null ? null : (ref, suspendContext, thread) -> { + final Value value = ref.invokeMethod(thread, hashCode, Collections.emptyList(), ClassType.INVOKE_SINGLE_THREADED); + return Optional.of(((IntegerValue) value).value()); + }; + } + + return myHashCodeWrapper; + } + + // WAIT_HASHES -> WAIT_UPDATE -> READY + enum MyState { + WAIT_HASHES { + @Override + public MyState next() { + return WAIT_UPDATE; + } + }, WAIT_UPDATE { + @Override + public MyState next() { + return READY; + } + }, READY { + @Override + public MyState next() { + return this; + } + }; + + public abstract MyState next(); + } + + private interface MyHashCodeMethodWrapper { + Optional eval(@NotNull ObjectReference ref, @NotNull SuspendContextImpl suspendContext, + @NotNull ThreadReference thread) throws InvocationException, InvalidTypeException, + ClassNotLoadedException, IncompatibleThreadStateException; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/IdentityBasedTracking.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/IdentityBasedTracking.java new file mode 100644 index 000000000000..32e5170103d5 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/IdentityBasedTracking.java @@ -0,0 +1,37 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.engine.SuspendContextImpl; +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.stream.Collectors; + +class IdentityBasedTracking extends InstanceTrackingStrategy { + private long myLastId = -1; + private int myCount = 0; + IdentityBasedTracking(@NotNull List initialInstances) { + myLastId = getMaxId(initialInstances); + } + + @NotNull + @Override + protected List updateImpl(@NotNull SuspendContextImpl suspendContext, + @NotNull List references) { + List result = references.stream() + .filter(reference -> reference.uniqueID() > myLastId) + .collect(Collectors.toList()); + myLastId = Math.max(myLastId, getMaxId(result)); + myCount = result.size(); + return result; + } + + private long getMaxId(@NotNull List references) { + return references.stream().map(ObjectReference::uniqueID).max(Long::compare).orElse(-1L); + } + + @Override + public int getCount() { + return myCount; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/InstanceTrackingStrategy.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/InstanceTrackingStrategy.java new file mode 100644 index 000000000000..9385105e4380 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/InstanceTrackingStrategy.java @@ -0,0 +1,52 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.engine.SuspendContextImpl; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class InstanceTrackingStrategy implements TrackerForNewInstances { + @NotNull + private List myNewInstances = new ArrayList<>(); + private boolean myIsReady = false; + + public static InstanceTrackingStrategy create(@NotNull ReferenceType referenceType, + @Nullable SuspendContextImpl suspendContext, + @NotNull TrackingType type, + @NotNull List init) { + switch (type) { + case IDENTITY: + return new IdentityBasedTracking(init); + case HASH: + return new HashBasedTracking(suspendContext, referenceType, init); + case RETAIN: + return new RetainReferencesTracking(init); + } + + throw new UnsupportedOperationException("Such TrackingType not found"); + } + + @NotNull + public final List getNewInstances() { + return Collections.unmodifiableList(myNewInstances); + } + + public final void update(@NotNull SuspendContextImpl suspendContext, @NotNull List references) { + myNewInstances = updateImpl(suspendContext, references); + myIsReady = true; + } + + @Override + public boolean isReady() { + return myIsReady; + } + + @NotNull + protected abstract List updateImpl(@NotNull SuspendContextImpl suspendContext, + @NotNull List references); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/RetainReferencesTracking.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/RetainReferencesTracking.java new file mode 100644 index 000000000000..c35070e95d7d --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/RetainReferencesTracking.java @@ -0,0 +1,30 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.debugger.engine.SuspendContextImpl; +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +class RetainReferencesTracking extends InstanceTrackingStrategy { + private final IdentityBasedTracking myIdTracker; + private List myHardRefs; + RetainReferencesTracking(@NotNull List initialInstances) { + myHardRefs = initialInstances; + myIdTracker = new IdentityBasedTracking(initialInstances); + } + + @NotNull + @Override + protected List updateImpl(@NotNull SuspendContextImpl suspendContext, + @NotNull List references) { + myIdTracker.update(suspendContext, references); + myHardRefs = references; + return myIdTracker.getNewInstances(); + } + + @Override + public int getCount() { + return myHardRefs.size(); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackerForNewInstances.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackerForNewInstances.java new file mode 100644 index 000000000000..b6fc329f5d36 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackerForNewInstances.java @@ -0,0 +1,15 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface TrackerForNewInstances { + @NotNull + List getNewInstances(); + + int getCount(); + + boolean isReady(); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackingType.java b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackingType.java new file mode 100644 index 000000000000..a000daaade40 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/tracking/TrackingType.java @@ -0,0 +1,32 @@ +package org.jetbrains.debugger.memory.tracking; + +import com.intellij.ui.JBColor; +import org.jetbrains.annotations.NotNull; + +public enum TrackingType { + + IDENTITY("Id Based Tracking",JBColor.yellow), + HASH("Hash Based Tracking", JBColor.yellow), + RETAIN("Retaining Reference Object Tracking", JBColor.yellow), + CREATION("Track Constructors", JBColor.orange); + + private final String myDescription; + private final JBColor myHighlighting; + + TrackingType(@NotNull String description, @NotNull JBColor highlighting) { + myDescription = description; + myHighlighting = highlighting; + } + + @NotNull + public JBColor color() { + return myHighlighting; + } + + @NotNull + public String description() { + return myDescription; + } + + public static JBColor inactiveColor() { return JBColor.GRAY; } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableColumnDescriptor.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableColumnDescriptor.java new file mode 100644 index 000000000000..3abb30a98e89 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableColumnDescriptor.java @@ -0,0 +1,21 @@ +package org.jetbrains.debugger.memory.utils; + +public abstract class AbstractTableColumnDescriptor implements AbstractTableModelWithColumns.TableColumnDescriptor { + private final String myName; + private final Class myClass; + protected AbstractTableColumnDescriptor(String name, Class elementClass) { + myName = name; + myClass = elementClass; + } + + @Override + public Class getColumnClass() { + return myClass; + } + + + @Override + public String getName() { + return myName; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableModelWithColumns.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableModelWithColumns.java new file mode 100644 index 000000000000..d0a67129078c --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AbstractTableModelWithColumns.java @@ -0,0 +1,37 @@ +package org.jetbrains.debugger.memory.utils; + +import javax.swing.table.AbstractTableModel; + +public abstract class AbstractTableModelWithColumns extends AbstractTableModel { + + private final TableColumnDescriptor[] myColumnDescriptors; + public AbstractTableModelWithColumns(TableColumnDescriptor[] descriptors) { + myColumnDescriptors = descriptors; + } + + @Override + public Class getColumnClass(int columnIndex) { + return myColumnDescriptors[columnIndex].getColumnClass(); + } + + @Override + public String getColumnName(int column) { + return myColumnDescriptors[column].getName(); + } + + @Override + public int getColumnCount() { + return myColumnDescriptors.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return myColumnDescriptors[columnIndex].getValue(rowIndex); + } + + interface TableColumnDescriptor { + Class getColumnClass(); + Object getValue(int ix); + String getName(); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/AndroidUtil.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AndroidUtil.java new file mode 100644 index 000000000000..112297fd4a96 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/AndroidUtil.java @@ -0,0 +1,13 @@ +package org.jetbrains.debugger.memory.utils; + +import com.sun.jdi.VirtualMachine; +import org.jetbrains.annotations.NotNull; + +public class AndroidUtil { + public static final int ANDROID_COUNT_BY_CLASSES_BATCH_SIZE = 500; + public static final int ANDROID_INSTANCES_LIMIT = 30000; + + public static boolean isAndroidVM(@NotNull VirtualMachine vm) { + return vm.name().toLowerCase().contains("dalvik"); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/ErrorsValueGroup.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/ErrorsValueGroup.java new file mode 100644 index 000000000000..922a70cf9af3 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/ErrorsValueGroup.java @@ -0,0 +1,65 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.icons.AllIcons; +import com.intellij.util.containers.HashMap; +import com.intellij.xdebugger.frame.XCompositeNode; +import com.intellij.xdebugger.frame.XNamedValue; +import com.intellij.xdebugger.frame.XValueChildrenList; +import com.intellij.xdebugger.frame.XValueGroup; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ErrorsValueGroup extends XValueGroup { + private final Map> myErrorMessage2ValueMap = new HashMap<>(); + + public ErrorsValueGroup(@NotNull String name) { + super(name); + } + + public void addErrorValue(@NotNull String message, @NotNull XNamedValue value) { + List lst; + if (!myErrorMessage2ValueMap.containsKey(message)) { + myErrorMessage2ValueMap.put(message, new ArrayList<>()); + } + + lst = myErrorMessage2ValueMap.get(message); + lst.add(value); + } + + public boolean isEmpty() { + return myErrorMessage2ValueMap.isEmpty(); + } + + @Nullable + @Override + public Icon getIcon() { + return AllIcons.General.Error; + } + + @Override + public void computeChildren(@NotNull XCompositeNode node) { + XValueChildrenList lst = new XValueChildrenList(); + myErrorMessage2ValueMap.keySet().forEach(s -> lst.addTopGroup(new MyErrorsValueGroup(s))); + node.addChildren(lst, true); + } + + private class MyErrorsValueGroup extends XValueGroup { + + @Override + public void computeChildren(@NotNull XCompositeNode node) { + XValueChildrenList lst = new XValueChildrenList(); + String name = getName(); + myErrorMessage2ValueMap.get(name).forEach(lst::add); + node.addChildren(lst, true); + } + + MyErrorsValueGroup(@NotNull String name) { + super(name); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceJavaValue.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceJavaValue.java new file mode 100644 index 000000000000..0c8046e6c989 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceJavaValue.java @@ -0,0 +1,31 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.debugger.engine.JavaValue; +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; +import com.intellij.debugger.ui.impl.watch.NodeManagerImpl; +import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl; +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class InstanceJavaValue extends JavaValue { + public InstanceJavaValue(JavaValue javaValue, + @NotNull ValueDescriptorImpl valueDescriptor, + @NotNull EvaluationContextImpl evaluationContext, + NodeManagerImpl nodeManager, + boolean contextSet) { + super(javaValue, valueDescriptor, evaluationContext, nodeManager, contextSet); + } + + @Nullable + @Override + public String getEvaluationExpression() { + ObjectReference ref = ((ObjectReference)getDescriptor().getValue()); + return NamesUtils.getUniqueName(ref); + } + + @Override + public boolean canNavigateToSource() { + return false; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceValueDescriptor.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceValueDescriptor.java new file mode 100644 index 000000000000..101d30714643 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstanceValueDescriptor.java @@ -0,0 +1,51 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.debugger.DebuggerContext; +import com.intellij.debugger.engine.ContextUtil; +import com.intellij.debugger.engine.evaluation.EvaluateException; +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; +import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl; +import com.intellij.openapi.project.Project; +import com.intellij.psi.JavaPsiFacade; +import com.intellij.psi.PsiElementFactory; +import com.intellij.psi.PsiExpression; +import com.sun.jdi.ArrayReference; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.Value; + +public class InstanceValueDescriptor extends ValueDescriptorImpl { + + public InstanceValueDescriptor(Project project, Value value) { + super(project, value); + } + + @Override + public String calcValueName() { + ObjectReference ref = ((ObjectReference) getValue()); + if (ref instanceof ArrayReference) { + ArrayReference arrayReference = (ArrayReference) ref; + return NamesUtils.getArrayUniqueName(arrayReference); + } + return NamesUtils.getUniqueName(ref); + } + + @Override + public Value calcValue(EvaluationContextImpl evaluationContext) throws EvaluateException { + return getValue(); + } + + @Override + public boolean isShowIdLabel() { + return false; + } + + @Override + public PsiExpression getDescriptorEvaluation(DebuggerContext debuggerContext) throws EvaluateException { + PsiElementFactory elementFactory = JavaPsiFacade.getInstance(myProject).getElementFactory(); + ObjectReference ref = ((ObjectReference) getValue()); + String name = NamesUtils.getUniqueName(ref).replace("@", ""); + String presentation = String.format("%s_DebugLabel", name); + + return elementFactory.createExpressionFromText(presentation, ContextUtil.getContextElement(debuggerContext)); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstancesProvider.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstancesProvider.java new file mode 100644 index 000000000000..1a717474b1b8 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/InstancesProvider.java @@ -0,0 +1,11 @@ +package org.jetbrains.debugger.memory.utils; + +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface InstancesProvider { + @NotNull + List getInstances(int limit); +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/KeyboardUtils.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/KeyboardUtils.java new file mode 100644 index 000000000000..57899d96122e --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/KeyboardUtils.java @@ -0,0 +1,21 @@ +package org.jetbrains.debugger.memory.utils; + +import java.awt.event.KeyEvent; + +public class KeyboardUtils { + public static boolean isEnterKey(int keyCode) { + return keyCode == KeyEvent.VK_ENTER; + } + + public static boolean isUpDownKey(int keyCode) { + return keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN; + } + + public static boolean isBackSpace(int keyCode) { + return keyCode == KeyEvent.VK_BACK_SPACE; + } + + public static boolean isCharacter(int keyCode) { + return KeyEvent.getKeyText(keyCode).length() == 1; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/LowestPriorityCommand.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/LowestPriorityCommand.java new file mode 100644 index 000000000000..214cc5681f66 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/LowestPriorityCommand.java @@ -0,0 +1,20 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.debugger.engine.SuspendContextImpl; +import com.intellij.debugger.engine.events.SuspendContextCommandImpl; +import org.jetbrains.annotations.Nullable; + +public class LowestPriorityCommand extends SuspendContextCommandImpl { + protected LowestPriorityCommand(@Nullable SuspendContextImpl suspendContext) { + super(suspendContext); + } + + @Override + protected void commandCancelled() { + } + + @Override + public Priority getPriority() { + return Priority.LOWEST; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/NamesUtils.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/NamesUtils.java new file mode 100644 index 000000000000..d7fdd334bf7d --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/NamesUtils.java @@ -0,0 +1,26 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.openapi.util.text.StringUtil; +import com.sun.jdi.ArrayReference; +import com.sun.jdi.ObjectReference; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +class NamesUtils { + @NotNull + static String getUniqueName(@NotNull ObjectReference ref) { + String shortName = StringUtil.getShortName(ref.referenceType().name()); + String name = shortName.replace("[]", "Array"); + return String.format("%s@%d", name, ref.uniqueID()); + } + + @NotNull + static String getArrayUniqueName(@NotNull ArrayReference ref) { + String shortName = StringUtil.getShortName(ref.referenceType().name()); + int length = ref.length(); + + String name = shortName.replaceFirst(Pattern.quote("[]"), String.format("[%d]", length)); + return String.format("%s@%d", name, ref.uniqueID()); + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/SingleAlarmWithMutableDelay.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/SingleAlarmWithMutableDelay.java new file mode 100644 index 000000000000..321cf46828c8 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/SingleAlarmWithMutableDelay.java @@ -0,0 +1,25 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.openapi.Disposable; +import com.intellij.util.Alarm; +import org.jetbrains.annotations.NotNull; + +public class SingleAlarmWithMutableDelay extends Alarm { + private final Runnable myTask; + private volatile int myDelay; + public SingleAlarmWithMutableDelay(@NotNull Runnable task, @NotNull Disposable parentDisposable) { + super(ThreadToUse.POOLED_THREAD, parentDisposable); + myTask = task; + } + + public void setDelay(int value) { + myDelay = value; + } + + public void cancelAndRequest() { + if(!isDisposed()) { + cancelAllRequests(); + addRequest(myTask, myDelay); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameDescriptor.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameDescriptor.java new file mode 100644 index 000000000000..7d1ca9d6ebef --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameDescriptor.java @@ -0,0 +1,40 @@ +package org.jetbrains.debugger.memory.utils; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +public class StackFrameDescriptor { + private final String myFilePath; + private final String myMethodName; + private final int myLineNumber; + + public StackFrameDescriptor(@NotNull String path, @NotNull String methodName, int line) { + myFilePath = path.replace('\\', '.'); + myMethodName = methodName; + myLineNumber = line; + } + + @NotNull + public String path() { + return myFilePath; + } + + @NotNull + public String methodName() { + return myMethodName; + } + + @NotNull + public String className() { + return StringUtil.getShortName(myFilePath); + } + + @NotNull + public String packageName() { + return StringUtil.getPackageName(myFilePath); + } + + public int line() { + return myLineNumber; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesFilteredView.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesFilteredView.java new file mode 100644 index 000000000000..92b5268239fb --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesFilteredView.java @@ -0,0 +1,490 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.debugger.DebuggerManager; +import com.intellij.debugger.engine.DebugProcessImpl; +import com.intellij.debugger.engine.SuspendContextImpl; +import com.intellij.debugger.engine.events.DebuggerCommandImpl; +import com.intellij.debugger.impl.DebuggerManagerImpl; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionPopupMenu; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.ui.*; +import com.intellij.util.SmartList; +import com.intellij.util.ui.components.BorderLayoutPanel; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebugSessionListener; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.VirtualMachine; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.component.CreationPositionTracker; +import org.jetbrains.debugger.memory.component.InstancesTracker; +import org.jetbrains.debugger.memory.component.MemoryViewManager; +import org.jetbrains.debugger.memory.component.MemoryViewManagerState; +import org.jetbrains.debugger.memory.event.InstancesTrackerListener; +import org.jetbrains.debugger.memory.event.MemoryViewManagerListener; +import org.jetbrains.debugger.memory.tracking.ClassPreparedListener; +import org.jetbrains.debugger.memory.tracking.ConstructorInstancesTracker; +import org.jetbrains.debugger.memory.tracking.TrackerForNewInstances; +import org.jetbrains.debugger.memory.tracking.TrackingType; +import org.jetbrains.debugger.memory.utils.AndroidUtil; +import org.jetbrains.debugger.memory.utils.KeyboardUtils; +import org.jetbrains.debugger.memory.utils.LowestPriorityCommand; +import org.jetbrains.debugger.memory.utils.SingleAlarmWithMutableDelay; + +import javax.swing.FocusManager; +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import java.awt.*; +import java.awt.event.*; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import static org.jetbrains.debugger.memory.view.ClassesTable.DiffViewTableModel.CLASSNAME_COLUMN_INDEX; +import static org.jetbrains.debugger.memory.view.ClassesTable.DiffViewTableModel.DIFF_COLUMN_INDEX; + +public class ClassesFilteredView extends BorderLayoutPanel implements Disposable { + private static final Logger LOG = Logger.getInstance(ClassesFilteredView.class); + private final static double DELAY_BEFORE_INSTANCES_QUERY_COEFFICIENT = 0.5; + private final static double MAX_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(2); + private final static int DEFAULT_BATCH_SIZE = Integer.MAX_VALUE; + private static final String EMPTY_TABLE_CONTENT_WHEN_RUNNING = "The application is running"; + private static final String EMPTY_TABLE_CONTENT_WHEN_SUSPENDED = "Nothing to show"; + + private final Project myProject; + private final XDebugSession myDebugSession; + private final DebugProcessImpl myDebugProcess; + private final SingleAlarmWithMutableDelay mySingleAlarm; + + private final SearchTextField myFilterTextField = new FilterTextField(); + private final ClassesTable myTable; + private final InstancesTracker myInstancesTracker; + private final Map myConstructorTrackedClasses = new ConcurrentHashMap<>(); + private final XDebugSessionListener myDebugSessionListener; + + @Nullable + private volatile SuspendContextImpl myLastSuspendContext; + + /** + * Indicates that the debug session had been stopped at least once. + *

+ * State: false to true + */ + private volatile boolean myIsTrackersActivated = false; + + /** + * Indicates that view is visible in tool window + */ + private boolean myIsActive; + + public ClassesFilteredView(@NotNull XDebugSession debugSession) { + super(); + + myProject = debugSession.getProject(); + myDebugSession = debugSession; + myDebugProcess = (DebugProcessImpl) DebuggerManager.getInstance(myProject) + .getDebugProcess(myDebugSession.getDebugProcess().getProcessHandler()); + + if (myDebugProcess == null) { + throw new NullPointerException("Failed to receive a java debug process"); + } + + myInstancesTracker = InstancesTracker.getInstance(myProject); + InstancesTrackerListener instancesTrackerListener = new InstancesTrackerListener() { + @Override + public void classChanged(@NotNull String name, @NotNull TrackingType type) { + ReferenceType ref = myTable.getClassByName(name); + if (ref != null) { + myDebugProcess.getManagerThread() + .schedule(new LowestPriorityCommand(getSuspendContext()) { + @Override + public void contextAction(@NotNull SuspendContextImpl suspendContext) throws Exception { + trackClass(ref, type, myIsTrackersActivated); + } + }); + } + myTable.repaint(); + } + + @Override + public void classRemoved(@NotNull String name) { + ReferenceType ref = myTable.getClassByName(name); + if (ref != null && myConstructorTrackedClasses.containsKey(ref)) { + ConstructorInstancesTracker removed = myConstructorTrackedClasses.remove(ref); + Disposer.dispose(removed); + myTable.getRowSorter().allRowsChanged(); + } + } + }; + + myDebugProcess.getManagerThread().schedule(new DebuggerCommandImpl() { + @Override + public Priority getPriority() { + return Priority.LOWEST; + } + + @Override + protected void action() throws Exception { + for (Map.Entry entry : myInstancesTracker.getTrackedClasses().entrySet()) { + TrackingType type = entry.getValue(); + String className = entry.getKey(); + List classes = myDebugProcess.getVirtualMachineProxy().classesByName(className); + if (classes.isEmpty()) { + new ClassPreparedListener(className, myDebugSession) { + @Override + public void onClassPrepared(@NotNull ReferenceType referenceType) { + trackClass(referenceType, type, myIsTrackersActivated); + } + }; + } else { + for (ReferenceType ref : classes) { + trackClass(ref, type, myIsTrackersActivated); + } + } + } + + myInstancesTracker.addTrackerListener(instancesTrackerListener, ClassesFilteredView.this); + } + }); + + MemoryViewManagerState memoryViewManagerState = MemoryViewManager.getInstance().getState(); + + myTable = new ClassesTable(myDebugSession, memoryViewManagerState.isShowWithDiffOnly, + memoryViewManagerState.isShowWithInstancesOnly, memoryViewManagerState.isShowTrackedOnly, this); + myTable.getEmptyText().setText(EMPTY_TABLE_CONTENT_WHEN_RUNNING); + Disposer.register(this, myTable); + + myTable.addMouseMotionListener(new MyMouseMotionListener()); + myTable.addMouseListener(new MyOpenNewInstancesListener()); + new MyDoubleClickListener().installOn(myTable); + + myTable.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + int keyCode = e.getKeyCode(); + if (KeyboardUtils.isEnterKey(keyCode)) { + handleClassSelection(myTable.getSelectedClass()); + } else if (KeyboardUtils.isCharacter(keyCode) || KeyboardUtils.isBackSpace(keyCode)) { + String text = myFilterTextField.getText(); + String newText = KeyboardUtils.isBackSpace(keyCode) + ? text.substring(0, text.length() - 1) + : text + e.getKeyChar(); + myFilterTextField.setText(newText); + FocusManager.getCurrentManager().focusNextComponent(myFilterTextField); + } + } + }); + + myFilterTextField.addKeyboardListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + dispatch(e); + } + + @Override + public void keyReleased(KeyEvent e) { + dispatch(e); + } + + private void dispatch(KeyEvent e) { + if (KeyboardUtils.isUpDownKey(e.getKeyCode()) || KeyboardUtils.isEnterKey(e.getKeyCode())) { + myTable.dispatchEvent(e); + } + } + }); + + myFilterTextField.addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(DocumentEvent e) { + myTable.setFilterPattern(myFilterTextField.getText()); + } + }); + + final MemoryViewManagerListener memoryViewManagerListener = state -> { + myTable.setFilteringByDiffNonZero(state.isShowWithDiffOnly); + myTable.setFilteringByInstanceExists(state.isShowWithInstancesOnly); + myTable.setFilteringByTrackingState(state.isShowTrackedOnly); + }; + + MemoryViewManager.getInstance().addMemoryViewManagerListener(memoryViewManagerListener, this); + + myDebugSessionListener = new XDebugSessionListener() { + @Override + public void sessionResumed() { + myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::obsolete); + SwingUtilities.invokeLater(() -> { + myTable.getEmptyText().setText(EMPTY_TABLE_CONTENT_WHEN_RUNNING); + myTable.hideContent(); + }); + mySingleAlarm.cancelAllRequests(); + } + + @Override + public void sessionStopped() { + debugSession.removeSessionListener(this); + } + + @Override + public void sessionPaused() { + SwingUtilities.invokeLater(() -> myTable.getEmptyText().setText(EMPTY_TABLE_CONTENT_WHEN_SUSPENDED)); + updateClassesAndCounts(); + } + }; + + mySingleAlarm = new SingleAlarmWithMutableDelay(() -> { + myLastSuspendContext = getSuspendContext(); + if (myLastSuspendContext != null) { + SwingUtilities.invokeLater(() -> myTable.setBusy(true)); + myDebugProcess.getManagerThread() + .schedule(new MyUpdateClassesCommand(myLastSuspendContext)); + } + }, this); + + myTable.addMouseListener(new PopupHandler() { + @Override + public void invokePopup(Component comp, int x, int y) { + ActionPopupMenu menu = createContextMenu(); + if (menu != null) { + menu.getComponent().show(comp, x, y); + } + } + }); + + JScrollPane scroll = ScrollPaneFactory.createScrollPane(myTable, SideBorder.TOP); + addToTop(myFilterTextField); + addToCenter(scroll); + } + + @Nullable + TrackerForNewInstances getStrategy(@NotNull ReferenceType ref) { + return myConstructorTrackedClasses.getOrDefault(ref, null); + } + + private void trackClass(@NotNull ReferenceType ref, + @NotNull TrackingType type, + boolean isTrackerEnabled) { + LOG.assertTrue(DebuggerManager.getInstance(myProject).isDebuggerManagerThread()); + if (type == TrackingType.CREATION) { + ConstructorInstancesTracker old = myConstructorTrackedClasses.getOrDefault(ref, null); + if (old != null) { + Disposer.dispose(old); + } + + ConstructorInstancesTracker tracker = new ConstructorInstancesTracker(ref, myDebugSession); + tracker.setBackgroundMode(!myIsActive); + if (isTrackerEnabled) { + tracker.enable(); + } else { + tracker.disable(); + } + + Disposer.register(ClassesFilteredView.this, tracker); + myConstructorTrackedClasses.put(ref, tracker); + } + } + + private void handleClassSelection(@Nullable ReferenceType ref) { + if (ref != null && myDebugSession.isSuspended()) { + new InstancesWindow(myDebugSession, ref::instances, ref.name()).show(); + } + } + + private void commitAllTrackers() { + myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::commitTracked); + } + + private SuspendContextImpl getSuspendContext() { + return DebuggerManagerImpl.getInstanceEx(myProject).getContext().getSuspendContext(); + } + + private void updateClassesAndCounts() { + if (myDebugProcess.isAttached()) { + mySingleAlarm.cancelAndRequest(); + } + } + + private ActionPopupMenu createContextMenu() { + ActionGroup group = (ActionGroup) ActionManager.getInstance().getAction("ClassesView.PopupActionGroup"); + return ActionManager.getInstance().createActionPopupMenu("ClassesView.PopupActionGroup", group); + } + + @Override + public void dispose() { + } + + public void setActive(boolean active) { + if (myIsActive == active) { + return; + } + + myIsActive = active; + myDebugProcess.getManagerThread().schedule(new DebuggerCommandImpl() { + @Override + protected void action() throws Exception { + if (active) { + doActivate(); + } else { + doPause(); + } + } + }); + } + + private void doActivate() { + myDebugSession.addSessionListener(myDebugSessionListener, ClassesFilteredView.this); + myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(false)); + final SuspendContextImpl lastContext = myLastSuspendContext; + if (lastContext == null || !lastContext.equals(getSuspendContext())) { + updateClassesAndCounts(); + } + } + + private void doPause() { + myDebugSession.removeSessionListener(myDebugSessionListener); + myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(true)); + } + + private final class MyUpdateClassesCommand extends LowestPriorityCommand { + + MyUpdateClassesCommand(@Nullable SuspendContextImpl suspendContext) { + super(suspendContext); + } + + @Override + public void contextAction(@NotNull SuspendContextImpl suspendContext) throws Exception { + if (!myIsTrackersActivated) { + myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::enable); + myIsTrackersActivated = true; + } else { + commitAllTrackers(); + } + final List classes = myDebugProcess.getVirtualMachineProxy().allClasses(); + + if (classes.isEmpty()) { + return; + } + + VirtualMachine vm = classes.get(0).virtualMachine(); + int batchSize = AndroidUtil.isAndroidVM(vm) + ? AndroidUtil.ANDROID_COUNT_BY_CLASSES_BATCH_SIZE + : DEFAULT_BATCH_SIZE; + + List chunks = new SmartList<>(); + int size = classes.size(); + for (int begin = 0, end = Math.min(batchSize, size); + begin != size && isContextValid(); + begin = end, end = Math.min(end + batchSize, size)) { + List batch = classes.subList(begin, end); + + long start = System.nanoTime(); + long[] counts = vm.instanceCounts(batch); + long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + chunks.add(counts); + + int waitTime = (int) Math.min(DELAY_BEFORE_INSTANCES_QUERY_COEFFICIENT * delay, MAX_DELAY_MILLIS); + mySingleAlarm.setDelay(waitTime); + LOG.info(String.format("Instances query time = %d ms. Count = %d", delay, batch.size())); + } + + if (isContextValid()) { + final long[] counts = chunks.size() == 1 ? chunks.get(0) : IntStream.range(0, chunks.size()).boxed() + .flatMapToLong(integer -> Arrays.stream(chunks.get(integer))) + .toArray(); + SwingUtilities.invokeLater(() -> myTable.setClassesAndUpdateCounts(classes, counts)); + } + + SwingUtilities.invokeLater(() -> myTable.setBusy(false)); + } + + private boolean isContextValid() { + return ClassesFilteredView.this.getSuspendContext() == getSuspendContext(); + } + } + + private class FilterTextField extends SearchTextField { + FilterTextField() { + super(false); + } + + @Override + protected void showPopup() { + } + + @Override + protected boolean hasIconsOutsideOfTextField() { + return false; + } + } + + private class MyOpenNewInstancesListener extends MouseAdapter { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() != 1 || e.getButton() != MouseEvent.BUTTON1 || !isShowNewInstancesEvent(e)) { + return; + } + + final CreationPositionTracker tracker = CreationPositionTracker.getInstance(myDebugSession.getProject()); + ReferenceType ref = myTable.getSelectedClass(); + TrackerForNewInstances strategy = ref == null ? null : getStrategy(ref); + if (strategy != null && tracker != null) { + List newInstances = strategy.getNewInstances(); + tracker.pinStacks(myDebugSession, ref); + InstancesWindow instancesWindow = new InstancesWindow(myDebugSession, limit -> newInstances, ref.name()); + Disposer.register(instancesWindow.getDisposable(), () -> tracker.unpinStacks(myDebugSession, ref)); + instancesWindow.show(); + } + } + } + + private class MyDoubleClickListener extends DoubleClickListener { + @Override + protected boolean onDoubleClick(MouseEvent event) { + if (!isShowNewInstancesEvent(event)) { + handleClassSelection(myTable.getSelectedClass()); + return true; + } + + return false; + } + } + + private class MyMouseMotionListener implements MouseMotionListener { + @Override + public void mouseDragged(MouseEvent e) { + } + + @Override + public void mouseMoved(MouseEvent e) { + if (isShowNewInstancesEvent(e)) { + myTable.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + myTable.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + } + + private boolean isShowNewInstancesEvent(@NotNull MouseEvent e) { + final int col = myTable.columnAtPoint(e.getPoint()); + final int row = myTable.rowAtPoint(e.getPoint()); + if (col == -1 || row == -1 || myTable.convertColumnIndexToModel(col) != DIFF_COLUMN_INDEX) { + return false; + } + + final int modelRow = myTable.convertRowIndexToModel(row); + + final ReferenceType ref = (ReferenceType) myTable.getModel().getValueAt(modelRow, CLASSNAME_COLUMN_INDEX); + final ConstructorInstancesTracker tracker = myConstructorTrackedClasses.getOrDefault(ref, null); + + return tracker != null && tracker.isReady() && tracker.getCount() > 0; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesTable.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesTable.java new file mode 100644 index 000000000000..cce435132c13 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/ClassesTable.java @@ -0,0 +1,482 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.DataKey; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.codeStyle.MinusculeMatcher; +import com.intellij.psi.codeStyle.NameUtil; +import com.intellij.ui.ColoredTableCellRenderer; +import com.intellij.ui.JBColor; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.speedSearch.SpeedSearchUtil; +import com.intellij.ui.table.JBTable; +import com.intellij.util.containers.FList; +import com.intellij.util.ui.JBDimension; +import com.intellij.util.ui.JBUI; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import icons.MemoryViewIcons; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.component.InstancesTracker; +import org.jetbrains.debugger.memory.tracking.TrackerForNewInstances; +import org.jetbrains.debugger.memory.tracking.TrackingType; +import org.jetbrains.debugger.memory.utils.AbstractTableColumnDescriptor; +import org.jetbrains.debugger.memory.utils.AbstractTableModelWithColumns; +import org.jetbrains.debugger.memory.utils.InstancesProvider; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.table.TableColumn; +import javax.swing.table.TableRowSorter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class ClassesTable extends JBTable implements DataProvider, Disposable { + public static final DataKey SELECTED_CLASS_KEY = DataKey.create("ClassesTable.SelectedClass"); + public static final DataKey DEBUG_SESSION_KEY = DataKey.create("ClassesTable.DebugSession"); + public static final DataKey NEW_INSTANCES_PROVIDER_KEY = + DataKey.create("ClassesTable.NewInstances"); + public static final DataKey REF_COUNT_PROVIDER_KEY = + DataKey.create("ClassesTable.ReferenceCountProvider"); + + private static final Border EMPTY_BORDER = BorderFactory.createEmptyBorder(); + + private static final int CLASSES_COLUMN_PREFERRED_WIDTH = 250; + private static final int COUNT_COLUMN_MIN_WIDTH = 80; + private static final int DIFF_COLUMN_MIN_WIDTH = 80; + + private final DiffViewTableModel myModel = new DiffViewTableModel(); + private final UnknownDiffValue myUnknownValue = new UnknownDiffValue(); + private final XDebugSession myDebugSession; + private final Map myCounts = new ConcurrentHashMap<>(); + private final InstancesTracker myInstancesTracker; + private final ClassesFilteredView myParent; + private final ReferenceCountProvider myCountProvider; + + private boolean myOnlyWithDiff; + private boolean myOnlyTracked; + private boolean myOnlyWithInstances; + private MinusculeMatcher myMatcher = NameUtil.buildMatcher("*").build(); + private String myFilteringPattern = ""; + + private volatile List myElems = Collections.unmodifiableList(new ArrayList<>()); + + ClassesTable(@NotNull XDebugSession session, boolean onlyWithDiff, boolean onlyWithInstances, + boolean onlyTracked, @NotNull ClassesFilteredView parent) { + setModel(myModel); + + myDebugSession = session; + myOnlyWithDiff = onlyWithDiff; + myOnlyWithInstances = onlyWithInstances; + myOnlyTracked = onlyTracked; + myInstancesTracker = InstancesTracker.getInstance(myDebugSession.getProject()); + myParent = parent; + + TableColumn classesColumn = getColumnModel().getColumn(DiffViewTableModel.CLASSNAME_COLUMN_INDEX); + TableColumn countColumn = getColumnModel().getColumn(DiffViewTableModel.COUNT_COLUMN_INDEX); + TableColumn diffColumn = getColumnModel().getColumn(DiffViewTableModel.DIFF_COLUMN_INDEX); + + setAutoResizeMode(AUTO_RESIZE_NEXT_COLUMN); + classesColumn.setPreferredWidth(JBUI.scale(CLASSES_COLUMN_PREFERRED_WIDTH)); + + countColumn.setMinWidth(JBUI.scale(COUNT_COLUMN_MIN_WIDTH)); + + diffColumn.setMinWidth(JBUI.scale(DIFF_COLUMN_MIN_WIDTH)); + + setShowGrid(false); + setIntercellSpacing(new JBDimension(0, 0)); + + setDefaultRenderer(ReferenceType.class, new MyClassColumnRenderer()); + setDefaultRenderer(Long.class, new MyCountColumnRenderer()); + setDefaultRenderer(DiffValue.class, new MyDiffColumnRenderer()); + + TableRowSorter sorter = new TableRowSorter<>(myModel); + sorter.setRowFilter(new RowFilter() { + @Override + public boolean include(Entry entry) { + int ix = entry.getIdentifier(); + ReferenceType ref = myElems.get(ix); + DiffValue diff = myCounts.getOrDefault(ref, myUnknownValue); + + boolean isFilteringOptionsRefused = myOnlyWithDiff && diff.diff() == 0 + || myOnlyWithInstances && !diff.hasInstance() + || myOnlyTracked && myParent.getStrategy(ref) == null; + return !(isFilteringOptionsRefused) && myMatcher.matches(ref.name()); + } + }); + + List myDefaultSortingKeys = Arrays.asList( + new RowSorter.SortKey(DiffViewTableModel.DIFF_COLUMN_INDEX, SortOrder.DESCENDING), + new RowSorter.SortKey(DiffViewTableModel.COUNT_COLUMN_INDEX, SortOrder.DESCENDING), + new RowSorter.SortKey(DiffViewTableModel.CLASSNAME_COLUMN_INDEX, SortOrder.ASCENDING) + ); + sorter.setSortKeys(myDefaultSortingKeys); + setRowSorter(sorter); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + myCountProvider = new ReferenceCountProvider() { + @Override + public int getTotalCount(@NotNull ReferenceType ref) { + return (int) myCounts.get(ref).myCurrentCount; + } + + @Override + public int getDiffCount(@NotNull ReferenceType ref) { + return (int) myCounts.get(ref).diff(); + } + + @Override + public int getNewInstancesCount(@NotNull ReferenceType ref) { + TrackerForNewInstances strategy = myParent.getStrategy(ref); + return strategy == null || !strategy.isReady() ? -1 : strategy.getCount(); + } + }; + } + + public interface ReferenceCountProvider { + int getTotalCount(@NotNull ReferenceType ref); + + int getDiffCount(@NotNull ReferenceType ref); + + int getNewInstancesCount(@NotNull ReferenceType ref); + } + + @Nullable + ReferenceType getSelectedClass() { + int selectedRow = getSelectedRow(); + if (selectedRow != -1) { + int ix = convertRowIndexToModel(selectedRow); + return myElems.get(ix); + } + + return null; + } + + @Nullable + ReferenceType getClassByName(@NotNull String name) { + for (ReferenceType ref : myElems) { + if (name.equals(ref.name())) { + return ref; + } + } + + return null; + } + + void setBusy(boolean value) { + setPaintBusy(value); + } + + void setFilterPattern(String pattern) { + if (!myFilteringPattern.equals(pattern)) { + myFilteringPattern = pattern; + myMatcher = NameUtil.buildMatcher("*" + pattern).build(); + getRowSorter().allRowsChanged(); + if (getSelectedClass() == null && getRowCount() > 0) { + getSelectionModel().setSelectionInterval(0, 0); + } + } + } + + void setFilteringByInstanceExists(boolean value) { + if (value != myOnlyWithInstances) { + myOnlyWithInstances = value; + getRowSorter().allRowsChanged(); + } + } + + void setFilteringByDiffNonZero(boolean value) { + if (myOnlyWithDiff != value) { + myOnlyWithDiff = value; + getRowSorter().allRowsChanged(); + } + } + + void setFilteringByTrackingState(boolean value) { + if (myOnlyTracked != value) { + myOnlyTracked = value; + getRowSorter().allRowsChanged(); + } + } + + void setClassesAndUpdateCounts(@NotNull List classes, @NotNull long[] counts) { + assert classes.size() == counts.length; + ReferenceType selectedClass = myModel.getSelectedClassBeforeHided(); + int newSelectedIndex = classes.indexOf(selectedClass); + boolean isInitialized = !myElems.isEmpty(); + myElems = Collections.unmodifiableList(new ArrayList<>(classes)); + + for (int i = 0, size = classes.size(); i < size; i++) { + ReferenceType ref = classes.get(i); + DiffValue oldValue = isInitialized && !myCounts.containsKey(ref) + ? new DiffValue(0, 0) + : myCounts.getOrDefault(ref, myUnknownValue); + myCounts.put(ref, oldValue.update(counts[i])); + } + + showContent(); + + if (newSelectedIndex != -1 && !myModel.isHided()) { + int ix = convertRowIndexToView(newSelectedIndex); + changeSelection(ix, + DiffViewTableModel.CLASSNAME_COLUMN_INDEX, false, false); + } + + getRowSorter().allRowsChanged(); + } + + void hideContent() { + myModel.hide(); + } + + private void showContent() { + myModel.show(); + } + + + @Nullable + @Override + public Object getData(@NonNls String dataId) { + if (SELECTED_CLASS_KEY.is(dataId)) { + return getSelectedClass(); + } + if (DEBUG_SESSION_KEY.is(dataId)) { + return myDebugSession; + } + if (NEW_INSTANCES_PROVIDER_KEY.is(dataId)) { + ReferenceType selectedClass = getSelectedClass(); + if (selectedClass != null) { + TrackerForNewInstances strategy = myParent.getStrategy(selectedClass); + if (strategy != null && strategy.isReady()) { + List newInstances = strategy.getNewInstances(); + return (InstancesProvider) limit -> newInstances; + } + } + } + + if (REF_COUNT_PROVIDER_KEY.is(dataId)) { + return myCountProvider; + } + + return null; + } + + @Override + public void dispose() { + } + + @Nullable + private TrackingType getTrackingType(int row) { + ReferenceType ref = (ReferenceType) getValueAt(row, convertColumnIndexToView(DiffViewTableModel.CLASSNAME_COLUMN_INDEX)); + return myInstancesTracker.getTrackingType(ref.name()); + } + + class DiffViewTableModel extends AbstractTableModelWithColumns { + final static int CLASSNAME_COLUMN_INDEX = 0; + final static int COUNT_COLUMN_INDEX = 1; + final static int DIFF_COLUMN_INDEX = 2; + + // Workaround: save selection after content of classes table has been hided + private ReferenceType mySelectedClassWhenHided = null; + private boolean myIsWithContent = false; + + DiffViewTableModel() { + super(new AbstractTableColumnDescriptor[]{ + new AbstractTableColumnDescriptor("Class", ReferenceType.class) { + @Override + public Object getValue(int ix) { + return myElems.get(ix); + } + }, + new AbstractTableColumnDescriptor("Count", Long.class) { + @Override + public Object getValue(int ix) { + return myCounts.getOrDefault(myElems.get(ix), myUnknownValue).myCurrentCount; + } + }, + new AbstractTableColumnDescriptor("Diff", DiffValue.class) { + @Override + public Object getValue(int ix) { + return myCounts.getOrDefault(myElems.get(ix), myUnknownValue); + } + } + }); + } + + ReferenceType getSelectedClassBeforeHided() { + return mySelectedClassWhenHided; + } + + void hide() { + if (myIsWithContent) { + mySelectedClassWhenHided = getSelectedClass(); + myIsWithContent = false; + clearSelection(); + fireTableDataChanged(); + } + } + + void show() { + if (!myIsWithContent) { + myIsWithContent = true; + getRowSorter().allRowsChanged(); + } + } + + boolean isHided() { + return !myIsWithContent; + } + + @Override + public int getRowCount() { + return myIsWithContent ? myElems.size() : 0; + } + } + + /** + * State transmissions for DiffValue and UnknownDiffValue + * unknown -> diff + * diff -> diff + *

+ * State descriptions: + * Unknown - instances count never executed + * Diff - actual value + */ + private static class UnknownDiffValue extends DiffValue { + UnknownDiffValue() { + super(0); + } + + @Override + boolean hasInstance() { + return true; + } + + @Override + DiffValue update(long count) { + return new DiffValue(count); + } + } + + private static class DiffValue implements Comparable { + private long myOldCount; + + private long myCurrentCount; + + DiffValue(long count) { + this(count, count); + } + + DiffValue(long old, long current) { + myCurrentCount = current; + myOldCount = old; + } + + DiffValue update(long count) { + myOldCount = myCurrentCount; + myCurrentCount = count; + return this; + } + + boolean hasInstance() { + return myCurrentCount > 0; + } + + long diff() { + return myCurrentCount - myOldCount; + } + + @Override + public int compareTo(@NotNull DiffValue o) { + return Long.compare(diff(), o.diff()); + } + } + + private abstract class MyTableCellRenderer extends ColoredTableCellRenderer { + @Override + protected void customizeCellRenderer(JTable table, @Nullable Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + column = convertColumnIndexToModel(column); + TrackingType trackingType = getTrackingType(row); + + if (hasFocus) { + setBorder(EMPTY_BORDER); + } + + if (trackingType != null && column == DiffViewTableModel.DIFF_COLUMN_INDEX) { + setIcon(MemoryViewIcons.CLASS_TRACKED); + setTransparentIconBackground(true); + } + + if (value != null) { + addText(table, value, isSelected, hasFocus, row, column); + } + } + + protected abstract void addText(JTable table, @NotNull Object value, boolean isSelected, + boolean hasFocus, int row, int column); + } + + private class MyClassColumnRenderer extends MyTableCellRenderer { + @Override + protected void addText(JTable table, @NotNull Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + String presentation = ((ReferenceType) value).name(); + append(" "); + if (isSelected) { + FList textRanges = myMatcher.matchingFragments(presentation); + if (textRanges != null) { + SimpleTextAttributes attributes = new SimpleTextAttributes(getBackground(), getForeground(), null, + SimpleTextAttributes.STYLE_SEARCH_MATCH); + SpeedSearchUtil.appendColoredFragments(this, presentation, textRanges, + SimpleTextAttributes.REGULAR_ATTRIBUTES, attributes); + } + } else { + append(String.format("%s", presentation), SimpleTextAttributes.REGULAR_ATTRIBUTES); + } + } + } + + private class MyCountColumnRenderer extends MyTableCellRenderer { + @Override + protected void addText(JTable table, @NotNull Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + setTextAlign(SwingConstants.RIGHT); + append(value.toString()); + } + } + + private class MyDiffColumnRenderer extends MyTableCellRenderer { + private final SimpleTextAttributes myClickableCellAttributes = + new SimpleTextAttributes(SimpleTextAttributes.STYLE_UNDERLINE, JBColor.BLUE); + + @Override + protected void addText(JTable table, @NotNull Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + setTextAlign(SwingConstants.RIGHT); + + ReferenceType ref = myElems.get(convertRowIndexToModel(row)); + + long diff = myCountProvider.getDiffCount(ref); + String text = String.format("%s%d", diff > 0 ? "+" : "", diff); + + int newInstancesCount = myCountProvider.getNewInstancesCount(ref); + if (newInstancesCount >= 0) { + if (newInstancesCount == diff) { + append(text, diff == 0 ? SimpleTextAttributes.REGULAR_ATTRIBUTES : myClickableCellAttributes); + } else { + append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES); + if (newInstancesCount != 0) { + append(String.format(" (%d)", newInstancesCount), myClickableCellAttributes); + } + } + } else { + append(text, SimpleTextAttributes.REGULAR_ATTRIBUTES); + } + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/ExpressionEditorWithHistory.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/ExpressionEditorWithHistory.java new file mode 100644 index 000000000000..d11e0e100873 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/ExpressionEditorWithHistory.java @@ -0,0 +1,92 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.codeInsight.lookup.LookupManager; +import com.intellij.debugger.engine.DebuggerUtils; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CustomShortcutSet; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.PopupStep; +import com.intellij.openapi.ui.popup.util.BaseListPopupStep; +import com.intellij.psi.PsiClass; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.popup.list.ListPopupImpl; +import com.intellij.xdebugger.XExpression; +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider; +import com.intellij.xdebugger.impl.XSourcePositionImpl; +import com.intellij.xdebugger.impl.breakpoints.XExpressionImpl; +import com.intellij.xdebugger.impl.ui.XDebuggerExpressionEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; + +class ExpressionEditorWithHistory extends XDebuggerExpressionEditor { + private static final String HISTORY_ID_PREFIX = "filtering"; + + ExpressionEditorWithHistory(final @NotNull Project project, + final @NotNull String className, + final @NotNull XDebuggerEditorsProvider debuggerEditorsProvider, + final @Nullable Disposable parentDisposable) { + super(project, debuggerEditorsProvider, HISTORY_ID_PREFIX + className, null, + XExpressionImpl.EMPTY_EXPRESSION, false, true, true); + + new AnAction("InstancesWindow.ShowHistory") { + @Override + public void actionPerformed(AnActionEvent e) { + showHistory(); + } + + @Override + public void update(AnActionEvent e) { + e.getPresentation().setEnabled(LookupManager.getActiveLookup(getEditor()) == null); + } + }.registerCustomShortcutSet(CustomShortcutSet.fromString("DOWN"), getComponent(), parentDisposable); + + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + ApplicationManager.getApplication().runReadAction(() -> { + final PsiClass psiClass = DebuggerUtils.findClass(className, + project, GlobalSearchScope.allScope(project)); + XSourcePositionImpl position = XSourcePositionImpl.createByElement(psiClass); + SwingUtilities.invokeLater(() -> setSourcePosition(position)); + }); + return null; + } + }.execute(); + } + + private void showHistory() { + List expressions = getRecentExpressions(); + if (!expressions.isEmpty()) { + ListPopupImpl historyPopup = new ListPopupImpl(new BaseListPopupStep(null, expressions) { + @Override + public PopupStep onChosen(XExpression selectedValue, boolean finalChoice) { + setExpression(selectedValue); + requestFocusInEditor(); + return FINAL_CHOICE; + } + }) { + @Override + protected ListCellRenderer getListElementRenderer() { + return new ColoredListCellRenderer() { + @Override + protected void customizeCellRenderer(@NotNull JList list, XExpression value, int index, + boolean selected, boolean hasFocus) { + append(value.getExpression()); + } + }; + } + }; + + historyPopup.getList().setFont(EditorUtil.getEditorFont()); + historyPopup.showUnderneathOf(getEditorComponent()); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/FilteringProgressView.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/FilteringProgressView.java new file mode 100644 index 000000000000..90c7b1b6091d --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/FilteringProgressView.java @@ -0,0 +1,119 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.icons.AllIcons; +import com.intellij.ui.JBProgressBar; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.UIUtil; +import com.intellij.util.ui.components.BorderLayoutPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.view.InstancesWindow.FilteringCompletionReason; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +class FilteringProgressView extends BorderLayoutPanel { + private final static String LIMIT_REACHED_LABEL = "Limit has been reached"; + private final static String INTERRUPTED_LABEL = "Filtering has been interrupted"; + + private final JBProgressBar myProgressBar = new JBProgressBar(); + private final BorderLayoutPanel myProgressPanel = new BorderLayoutPanel(); + private final JBLabel myStopButton = new JBLabel(UIUtil.isUnderDarcula() + ? AllIcons.Actions.Clean : AllIcons.Actions.CleanLight); + private final JBLabel myProgressText = new JBLabel(); + + private int myProceedCount = 0; + private int myErrorCount = 0; + private int myMatchedCount = 0; + + private boolean myIsInProcess = false; + + @Nullable + private FilteringCompletionReason myCompletionReason = null; + + FilteringProgressView() { + myStopButton.setOpaque(true); + myProgressPanel.addToCenter(myProgressBar); + myProgressPanel.addToRight(myStopButton); + addToTop(myProgressText); + addToCenter(myProgressPanel); + } + + void addStopActionListener(@NotNull Runnable listener) { + myStopButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + listener.run(); + } + }); + } + + void updateProgress(int proceedCount, int matchedCount, int errorCount) { + if (myIsInProcess) { + myProceedCount += proceedCount; + myMatchedCount += matchedCount; + myErrorCount += errorCount; + myProgressBar.setValue(myProceedCount); + myProgressText.setText(getDescription()); + } + } + + void start(int totalCount) { + reset(); + myIsInProcess = true; + myProgressBar.setMinimum(0); + myProgressBar.setMaximum(totalCount); + myProgressBar.setValue(0); + myProgressText.setText(getDescription()); + setProgressBarVisible(true); + } + + void complete(@NotNull FilteringCompletionReason reason) { + if (!myIsInProcess) { + throw new IllegalStateException("First you need to start progress"); + } + + myIsInProcess = false; + myCompletionReason = reason; + myProgressText.setText(getDescription()); + setProgressBarVisible(false); + } + + private void reset() { + myIsInProcess = false; + myErrorCount = 0; + myProceedCount = 0; + myMatchedCount = 0; + myCompletionReason = null; + } + + private void setProgressBarVisible(boolean value) { + myProgressPanel.setVisible(value); + } + + private String getDescription() { + int total = myProgressBar.getMaximum(); + String itemsInfo = String.format("Shown: %d / %d", myMatchedCount, total); + if (myIsInProcess || myCompletionReason == null) { + return itemsInfo; + } + + switch (myCompletionReason) { + case ALL_CHECKED: + break; + case INTERRUPTED: + itemsInfo = String.format("%s (%s)", itemsInfo, INTERRUPTED_LABEL); + break; + case LIMIT_REACHED: + itemsInfo = String.format("%s (%s)", itemsInfo, LIMIT_REACHED_LABEL); + break; + } + + if (!myIsInProcess && myErrorCount != 0) { + String errors = String.format("Errors: %d", myErrorCount); + return String.format("%s
%s", itemsInfo, errors); + } + + return itemsInfo; + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesTree.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesTree.java new file mode 100644 index 000000000000..6352ffe02c9a --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesTree.java @@ -0,0 +1,134 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.debugger.ui.impl.watch.NodeDescriptorProvider; +import com.intellij.debugger.ui.tree.NodeDescriptor; +import com.intellij.debugger.ui.tree.ValueDescriptor; +import com.intellij.openapi.actionSystem.DataKey; +import com.intellij.openapi.project.Project; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider; +import com.intellij.xdebugger.frame.*; +import com.intellij.xdebugger.impl.actions.XDebuggerActions; +import com.intellij.xdebugger.impl.frame.XValueMarkers; +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree; +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.Value; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.tree.TreePath; +import java.util.ArrayList; +import java.util.List; + +public class InstancesTree extends XDebuggerTree { + public static final DataKey DEBUG_SESSION_DATA_KEY = DataKey.create("InstancesTree.DebugSession"); + private final XValueNodeImpl myRoot; + private final Runnable myOnRootExpandAction; + private final XDebugSession myDebugSession; + private List myChildren; + + InstancesTree(@NotNull Project project, + @NotNull XDebugSession debugSession, + @NotNull XDebuggerEditorsProvider editorsProvider, + @Nullable XValueMarkers valueMarkers, + @NotNull Runnable onRootExpand) { + super(project, editorsProvider, null, XDebuggerActions.INSPECT_TREE_POPUP_GROUP, valueMarkers); + myOnRootExpandAction = onRootExpand; + myRoot = new XValueNodeImpl(this, null, "root", new MyRootValue()); + myDebugSession = debugSession; + + myRoot.children(); + setRoot(myRoot, false); + myRoot.setLeaf(false); + setSelectionRow(0); + expandNodesOnLoad(node -> node == myRoot); + } + + void addChildren(@NotNull XValueChildrenList children, boolean last) { + if (myChildren == null) { + myChildren = new ArrayList<>(); + } + + myChildren.add(children); + myRoot.addChildren(children, last); + } + + void rebuildTree(@NotNull RebuildPolicy policy, @NotNull XDebuggerTreeState state) { + if (policy == RebuildPolicy.RELOAD_INSTANCES) { + myChildren = null; + } + + rebuildAndRestore(state); + } + + void rebuildTree(@NotNull RebuildPolicy policy) { + rebuildTree(policy, XDebuggerTreeState.saveState(this)); + } + + void setMessage(@Nullable Icon icon, @NotNull String text, @NotNull SimpleTextAttributes textAttributes) { + myChildren = null; + myRoot.clearChildren(); + myRoot.setMessage(text, icon, textAttributes, null); + } + + @Nullable + ObjectReference getSelectedReference() { + TreePath selectionPath = getSelectionPath(); + Object selectedItem = selectionPath != null ? selectionPath.getLastPathComponent() : null; + if (selectedItem instanceof XValueNodeImpl) { + XValueNodeImpl xValueNode = (XValueNodeImpl) selectedItem; + XValue valueContainer = xValueNode.getValueContainer(); + + if (valueContainer instanceof NodeDescriptorProvider) { + NodeDescriptor descriptor = ((NodeDescriptorProvider) valueContainer).getDescriptor(); + + if (descriptor instanceof ValueDescriptor) { + Value value = ((ValueDescriptor) descriptor).getValue(); + + if (value instanceof ObjectReference) return (ObjectReference) value; + } + } + } + + return null; + } + + @Nullable + @Override + public Object getData(@NonNls String dataId) { + if (DEBUG_SESSION_DATA_KEY.is(dataId)) { + return myDebugSession; + } + + return super.getData(dataId); + } + + enum RebuildPolicy { + RELOAD_INSTANCES, ONLY_UPDATE_LABELS + } + + private class MyRootValue extends XValue { + @Override + public void computeChildren(@NotNull XCompositeNode node) { + if (myChildren == null) { + myOnRootExpandAction.run(); + } else { + for (XValueChildrenList children : myChildren) { + myRoot.addChildren(children, false); + } + + myRoot.addChildren(XValueChildrenList.EMPTY, true); + } + } + + @Override + public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) { + node.setPresentation(null, "", "", true); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWindow.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWindow.java new file mode 100644 index 000000000000..891ebbf82da7 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWindow.java @@ -0,0 +1,555 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.debugger.DebuggerManager; +import com.intellij.debugger.engine.DebugProcessImpl; +import com.intellij.debugger.engine.JavaValue; +import com.intellij.debugger.engine.SuspendContextImpl; +import com.intellij.debugger.engine.evaluation.EvaluateException; +import com.intellij.debugger.engine.evaluation.EvaluationContext; +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; +import com.intellij.debugger.engine.evaluation.TextWithImportsImpl; +import com.intellij.debugger.engine.evaluation.expression.ExpressionEvaluator; +import com.intellij.debugger.engine.events.DebuggerContextCommandImpl; +import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl; +import com.intellij.debugger.ui.impl.watch.MessageDescriptor; +import com.intellij.debugger.ui.impl.watch.NodeManagerImpl; +import com.intellij.debugger.ui.tree.NodeDescriptor; +import com.intellij.debugger.ui.tree.render.CachedEvaluator; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.AnActionListener; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.ui.DoubleClickListener; +import com.intellij.ui.JBColor; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBPanel; +import com.intellij.util.ui.JBDimension; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; +import com.intellij.util.ui.update.UiNotifyConnector; +import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebugSessionListener; +import com.intellij.xdebugger.XExpression; +import com.intellij.xdebugger.frame.XValue; +import com.intellij.xdebugger.frame.XValueChildrenList; +import com.intellij.xdebugger.impl.XDebugSessionImpl; +import com.intellij.xdebugger.impl.actions.XDebuggerActionBase; +import com.intellij.xdebugger.impl.frame.XValueMarkers; +import com.intellij.xdebugger.impl.ui.XDebuggerExpressionEditor; +import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants; +import com.intellij.xdebugger.impl.ui.tree.ValueMarkup; +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState; +import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.sun.jdi.BooleanValue; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.memory.utils.*; +import org.jetbrains.java.debugger.JavaDebuggerEditorsProvider; + +import javax.swing.*; +import javax.swing.tree.TreeNode; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.Math.min; + +public class InstancesWindow extends DialogWrapper { + private static final Logger LOG = Logger.getInstance(InstancesWindow.class); + + private static final int DEFAULT_WINDOW_WIDTH = 870; + private static final int DEFAULT_WINDOW_HEIGHT = 400; + private static final int FILTERING_BUTTON_ADDITIONAL_WIDTH = 30; + private static final int BORDER_LAYOUT_DEFAULT_GAP = 5; + private static final int DEFAULT_INSTANCES_LIMIT = 500000; // All + + private final Project myProject; + private final XDebugSession myDebugSession; + private final InstancesProvider myInstancesProvider; + private final String myClassName; + private MyInstancesView myInstancesView; + + public InstancesWindow(@NotNull XDebugSession session, + @NotNull InstancesProvider provider, + @NotNull String className) { + super(session.getProject(), false); + + myProject = session.getProject(); + myDebugSession = session; + myInstancesProvider = provider; + myClassName = className; + + addWarningMessage(null); + myDebugSession.addSessionListener(new XDebugSessionListener() { + @Override + public void sessionStopped() { + SwingUtilities.invokeLater(() -> close(OK_EXIT_CODE)); + } + }, myDisposable); + setModal(false); + init(); + JRootPane root = myInstancesView.getRootPane(); + root.setDefaultButton(myInstancesView.myFilterButton); + } + + enum FilteringCompletionReason { + ALL_CHECKED, INTERRUPTED, LIMIT_REACHED + } + + private void addWarningMessage(@Nullable String message) { + String warning = message == null ? "" : String.format(". Warning: %s", message); + setTitle(String.format("Instances of %s%s", myClassName, warning)); + } + + @NotNull + @Override + protected String getDimensionServiceKey() { + return "#org.jetbrains.debugger.memory.view.InstancesWindow"; + } + + @Nullable + @Override + protected JComponent createCenterPanel() { + myInstancesView = new MyInstancesView(); + myInstancesView.setPreferredSize( + new JBDimension(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)); + return myInstancesView; + } + + @Nullable + @Override + protected JComponent createSouthPanel() { + JComponent comp = super.createSouthPanel(); + if (comp != null) { + comp.add(myInstancesView.myProgress, BorderLayout.WEST); + } + + return comp; + } + + @NotNull + @Override + protected Action[] createActions() { + return new Action[]{new DialogWrapperExitAction("Close", CLOSE_EXIT_CODE)}; + } + + private class MyInstancesView extends JBPanel implements Disposable { + private static final int MAX_TREE_NODE_COUNT = 2000; + private static final int FILTERING_CHUNK_SIZE = 30; + + private final InstancesTree myInstancesTree; + private final XDebuggerExpressionEditor myFilterConditionEditor; + private final XDebugSessionListener myDebugSessionListener = new MySessionListener(); + + private final MyCachedEvaluator myEvaluator = new MyCachedEvaluator(); + private final MyNodeManager myNodeManager = new MyNodeManager(myProject); + + private final JButton myFilterButton = new JButton("Filter"); + private final FilteringProgressView myProgress = new FilteringProgressView(); + + private final AnActionListener.Adapter myActionListener = new MyActionListener(); + private final Object myFilteringTaskLock = new Object(); + + private boolean myIsAndroidVM = false; + + private volatile MyFilteringWorker myFilteringTask = null; + + MyInstancesView() { + super(new BorderLayout(0, JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP))); + + Disposer.register(InstancesWindow.this.myDisposable, MyInstancesView.this); + XValueMarkers markers = getValueMarkers(); + ActionManager.getInstance().addAnActionListener(myActionListener, InstancesWindow.this.myDisposable); + myDebugSession.addSessionListener(myDebugSessionListener, InstancesWindow.this.myDisposable); + JavaDebuggerEditorsProvider editorsProvider = new JavaDebuggerEditorsProvider(); + + myFilterConditionEditor = new ExpressionEditorWithHistory(myProject, myClassName, + editorsProvider, InstancesWindow.this.myDisposable); + + myFilterButton.setBorder(BorderFactory.createEmptyBorder()); + Dimension filteringButtonSize = myFilterConditionEditor.getEditorComponent().getPreferredSize(); + filteringButtonSize.width = JBUI.scale(FILTERING_BUTTON_ADDITIONAL_WIDTH) + + myFilterButton.getPreferredSize().width; + myFilterButton.setPreferredSize(filteringButtonSize); + + JBPanel filteringPane = new JBPanel(new BorderLayout(JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP), 0)); + JBLabel sideEffectsWarning = new JBLabel("Warning: filtering may have side effects", SwingConstants.RIGHT); + sideEffectsWarning.setBorder(JBUI.Borders.empty(1, 0, 0, 0)); + sideEffectsWarning.setComponentStyle(UIUtil.ComponentStyle.SMALL); + sideEffectsWarning.setFontColor(UIUtil.FontColor.BRIGHTER); + + filteringPane.add(new JBLabel("Condition:"), BorderLayout.WEST); + filteringPane.add(myFilterConditionEditor.getComponent(), BorderLayout.CENTER); + filteringPane.add(myFilterButton, BorderLayout.EAST); + filteringPane.add(sideEffectsWarning, BorderLayout.SOUTH); + + myProgress.addStopActionListener(this::cancelFilteringTask); + + myInstancesTree = new InstancesTree(myProject, myDebugSession, editorsProvider, markers, this::updateInstances); + + myFilterButton.addActionListener(e -> { + String expression = myFilterConditionEditor.getExpression().getExpression(); + if (!expression.isEmpty()) { + myFilterConditionEditor.saveTextInHistory(); + } + + myFilterButton.setEnabled(false); + myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES); + }); + + + StackFrameList list = new StackFrameList(myProject, + Collections.emptyList(), + GlobalSearchScope.allScope(myProject)); + + list.addListSelectionListener(e -> list.navigateToSelectedValue(false)); + new DoubleClickListener() { + @Override + protected boolean onDoubleClick(MouseEvent event) { + list.navigateToSelectedValue(true); + return true; + } + }.installOn(list); + + InstancesWithStackFrameView instancesWithStackFrame = new InstancesWithStackFrameView(myDebugSession, + myInstancesTree, list, myClassName); + + add(filteringPane, BorderLayout.NORTH); + add(instancesWithStackFrame.getComponent(), BorderLayout.CENTER); + + JComponent focusedComponent = myFilterConditionEditor.getEditorComponent(); + UiNotifyConnector.doWhenFirstShown(focusedComponent, () -> + IdeFocusManager.findInstanceByComponent(focusedComponent) + .requestFocus(focusedComponent, true)); + } + + @Override + public void dispose() { + cancelFilteringTask(); + Disposer.dispose(myInstancesTree); + } + + private void updateInstances() { + DebugProcessImpl debugProcess = (DebugProcessImpl) DebuggerManager.getInstance(myProject) + .getDebugProcess(myDebugSession.getDebugProcess().getProcessHandler()); + + cancelFilteringTask(); + + debugProcess.getManagerThread().schedule(new DebuggerContextCommandImpl(debugProcess.getDebuggerContext()) { + @Override + public Priority getPriority() { + return Priority.LOWEST; + } + + @Override + public void threadAction(@NotNull SuspendContextImpl suspendContext) { + myIsAndroidVM = AndroidUtil.isAndroidVM(debugProcess.getVirtualMachineProxy().getVirtualMachine()); + int limit = myIsAndroidVM + ? AndroidUtil.ANDROID_INSTANCES_LIMIT + : DEFAULT_INSTANCES_LIMIT; + List instances = myInstancesProvider.getInstances(limit + 1); + + EvaluationContextImpl evaluationContext = debugProcess + .getDebuggerContext().createEvaluationContext(); + + if (instances.size() > limit) { + addWarningMessage(String.format("Not all instances will be loaded (only %d)", limit)); + instances = instances.subList(0, limit); + } + + final int progressSize = instances.size(); + if (evaluationContext != null) { + synchronized (myFilteringTaskLock) { + myFilteringTask = new MyFilteringWorker(instances, evaluationContext, createEvaluator()); + SwingUtilities.invokeLater(() -> { + myProgress.start(progressSize); + myFilteringTask.execute(); + }); + } + } + } + }); + } + + @Nullable + private ExpressionEvaluator createEvaluator() { + ExpressionEvaluator evaluator = null; + + XExpression expression = myFilterConditionEditor.getExpression(); + if (expression != null && !expression.getExpression().isEmpty()) { + try { + myEvaluator.setReferenceExpression(TextWithImportsImpl.fromXExpression(expression)); + evaluator = myEvaluator.getEvaluator(); + } catch (EvaluateException ignored) { + LOG.info("cannot create evaluator by the expression = " + expression.getExpression()); + } + } + + return evaluator; + } + + private void cancelFilteringTask() { + if (myFilteringTask != null) { + synchronized (myFilteringTaskLock) { + if (myFilteringTask != null) { + myFilteringTask.cancel(true); + myFilteringTask = null; + } + } + } + } + + private XValueMarkers getValueMarkers() { + return myDebugSession instanceof XDebugSessionImpl + ? ((XDebugSessionImpl) myDebugSession).getValueMarkers() + : null; + } + + private class MySessionListener implements XDebugSessionListener { + private volatile XDebuggerTreeState myTreeState = null; + + @Override + public void sessionResumed() { + SwingUtilities.invokeLater(() -> { + myTreeState = XDebuggerTreeState.saveState(myInstancesTree); + cancelFilteringTask(); + + myInstancesTree.setMessage(XDebuggerUIConstants.INFORMATION_MESSAGE_ICON, + "The application is running", SimpleTextAttributes.REGULAR_ATTRIBUTES); + }); + } + + @Override + public void sessionPaused() { + SwingUtilities.invokeLater(() -> { + myProgress.setVisible(true); + myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES, myTreeState); + }); + } + } + + private class MyActionListener extends AnActionListener.Adapter { + @Override + public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { + if (dataContext.getData(PlatformDataKeys.CONTEXT_COMPONENT) == myInstancesView.myInstancesTree && + (isAddToWatchesAction(action) || isEvaluateExpressionAction(action))) { + XValueNodeImpl selectedNode = XDebuggerTreeActionBase.getSelectedNode(dataContext); + XValueMarkers markers = getValueMarkers(); + + if (markers != null && selectedNode != null) { + + TreeNode currentNode = selectedNode; + while (!myInstancesTree.getRoot().equals(currentNode.getParent())) { + currentNode = currentNode.getParent(); + } + + XValue valueContainer = ((XValueNodeImpl) currentNode).getValueContainer(); + + String expression = valueContainer.getEvaluationExpression(); + if (expression != null) { + markers.markValue(valueContainer, + new ValueMarkup(expression.replace("@", ""), new JBColor(0, 0), null)); + } + + SwingUtilities.invokeLater(() -> myInstancesTree + .rebuildTree(InstancesTree.RebuildPolicy.ONLY_UPDATE_LABELS)); + } + } + } + + private boolean isAddToWatchesAction(AnAction action) { + String className = action.getClass().getSimpleName(); + return action instanceof XDebuggerTreeActionBase && className.equals("XAddToWatchesAction"); + } + + private boolean isEvaluateExpressionAction(AnAction action) { + String className = action.getClass().getSimpleName(); + return action instanceof XDebuggerActionBase && className.equals("EvaluateAction"); + } + } + + private final class MyCachedEvaluator extends CachedEvaluator { + @Override + @NotNull + protected String getClassName() { + return myClassName; + } + + ExpressionEvaluator getEvaluator() throws EvaluateException { + return getEvaluator(myProject); + } + } + + private class MyFilteringWorker extends SwingWorker { + private final List myReferences; + private final ErrorsValueGroup myErrorsGroup = new ErrorsValueGroup("Errors"); + private final ExpressionEvaluator myExpressionEvaluator; + + private final EvaluationContextImpl myEvaluationContext; + private volatile boolean myDebuggerTaskCompleted = false; + + @NotNull + private volatile FilteringCompletionReason myCompletionReason = FilteringCompletionReason.INTERRUPTED; + + MyFilteringWorker(@NotNull List refs, + @NotNull EvaluationContextImpl evaluationContext, + @Nullable ExpressionEvaluator evaluator) { + myReferences = refs; + myEvaluationContext = evaluationContext; + myExpressionEvaluator = evaluator; + } + + @Override + protected void done() { + if (!myErrorsGroup.isEmpty()) { + XValueChildrenList lst = new XValueChildrenList(); + lst.addBottomGroup(myErrorsGroup); + myInstancesTree.addChildren(lst, true); + } else { + myInstancesTree.addChildren(XValueChildrenList.EMPTY, true); + } + + myFilterButton.setEnabled(true); + SwingUtilities.invokeLater(() -> myProgress.complete(myCompletionReason)); + } + + @Override + protected Void doInBackground() throws Exception { + DebugProcessImpl debugProcess = (DebugProcessImpl) DebuggerManager.getInstance(myProject) + .getDebugProcess(myDebugSession.getDebugProcess().getProcessHandler()); + + AtomicInteger totalChildren = new AtomicInteger(0); + AtomicInteger totalViewed = new AtomicInteger(0); + long start = System.nanoTime(); + for (int i = 0, size = myReferences.size(); i < size && totalChildren.get() < MAX_TREE_NODE_COUNT; + i += FILTERING_CHUNK_SIZE) { + myDebuggerTaskCompleted = false; + final int chunkBegin = i; + debugProcess.getManagerThread().schedule(new DebuggerContextCommandImpl(debugProcess.getDebuggerContext()) { + @Override + public Priority getPriority() { + return Priority.LOWEST; + } + + @Override + public void threadAction(@NotNull SuspendContextImpl suspendContext) { + // TODO: need to rewrite this + XValueChildrenList children = new XValueChildrenList(); + int endOfChunk = min(chunkBegin + FILTERING_CHUNK_SIZE, size); + int errorCount = 0; + for (int j = chunkBegin; j < endOfChunk && totalChildren.get() < MAX_TREE_NODE_COUNT; j++) { + ObjectReference ref = myReferences.get(j); + totalViewed.incrementAndGet(); + Pair comparison = null; + if (myExpressionEvaluator != null) { + comparison = isSatisfy(myExpressionEvaluator, ref); + if (comparison.first == MyFilteringResult.EVAL_ERROR) { + errorCount++; + } + + if (comparison.first == MyFilteringResult.NO_MATCH) { + continue; + } + } + + JavaValue val = new InstanceJavaValue(null, new InstanceValueDescriptor(myProject, ref), + myEvaluationContext, myNodeManager, true); + if (comparison == null || comparison.first == MyFilteringResult.MATCH) { + children.add(val); + } else { + myErrorsGroup.addErrorValue(comparison.second, val); + } + + totalChildren.incrementAndGet(); + } + + if (children.size() > 0) { + SwingUtilities.invokeLater(() -> { + if (MyFilteringWorker.this == myFilteringTask) { + myInstancesTree.addChildren(children, false); + } + }); + } + + final int childrenSize = children.size(); + final int finalErrorsCount = errorCount; + SwingUtilities.invokeLater(() -> + myProgress.updateProgress(endOfChunk - chunkBegin, childrenSize, finalErrorsCount)); + + synchronized (MyFilteringWorker.this) { + myDebuggerTaskCompleted = true; + MyFilteringWorker.this.notify(); + } + } + }); + + synchronized (MyFilteringWorker.this) { + while (!myDebuggerTaskCompleted) { + MyFilteringWorker.this.wait(); + } + } + } + + LOG.info(String.format("Filtering time = %d ms, count = %d, matched = %d", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), totalViewed.get(), totalChildren.get())); + + myCompletionReason = totalViewed.get() == myReferences.size() + ? FilteringCompletionReason.ALL_CHECKED + : FilteringCompletionReason.LIMIT_REACHED; + + return null; + } + + private Pair isSatisfy(@NotNull ExpressionEvaluator evaluator, @NotNull Value value) { + try { + Value result = evaluator.evaluate(myEvaluationContext.createEvaluationContext(value)); + if (result instanceof BooleanValue && ((BooleanValue) result).value()) { + return Pair.create(MyFilteringResult.MATCH, ""); + } + } catch (EvaluateException e) { + return Pair.create(MyFilteringResult.EVAL_ERROR, e.getMessage()); + } + + return Pair.create(MyFilteringResult.NO_MATCH, ""); + } + } + } + + private final static class MyNodeManager extends NodeManagerImpl { + MyNodeManager(Project project) { + super(project, null); + } + + @Override + public DebuggerTreeNodeImpl createNode(final NodeDescriptor descriptor, EvaluationContext evaluationContext) { + return new DebuggerTreeNodeImpl(null, descriptor); + } + + @Override + public DebuggerTreeNodeImpl createMessageNode(MessageDescriptor descriptor) { + return new DebuggerTreeNodeImpl(null, descriptor); + } + + @Override + public DebuggerTreeNodeImpl createMessageNode(String message) { + return new DebuggerTreeNodeImpl(null, new MessageDescriptor(message)); + } + } + + private enum MyFilteringResult { + MATCH, NO_MATCH, EVAL_ERROR + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWithStackFrameView.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWithStackFrameView.java new file mode 100644 index 000000000000..9ec7a35214b0 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/InstancesWithStackFrameView.java @@ -0,0 +1,130 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.ui.JBSplitter; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.ui.components.labels.ActionLink; +import com.intellij.xdebugger.XDebugSession; +import com.sun.jdi.ObjectReference; +import icons.MemoryViewIcons; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.memory.component.CreationPositionTracker; +import org.jetbrains.debugger.memory.component.InstancesTracker; +import org.jetbrains.debugger.memory.event.InstancesTrackerListener; +import org.jetbrains.debugger.memory.tracking.TrackingType; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; + +import javax.swing.*; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +class InstancesWithStackFrameView { + private static final float DEFAULT_SPLITTER_PROPORTION = 0.7f; + private static final String EMPTY_TEXT_WHEN_ITEM_NOT_SELECTED = "Select instance to see stack frame"; + private static final String EMPTY_TEXT_WHEN_STACK_NOT_FOUND = "No stack frame for this instance"; + private static final String TEXT_FOR_ARRAYS = "Arrays could not be tracked"; + private static final List EMPTY_FRAME = Collections.emptyList(); + + private float myHidedProportion; + + private final JBSplitter mySplitter = new JBSplitter(false, DEFAULT_SPLITTER_PROPORTION); + private boolean myIsHided = false; + + InstancesWithStackFrameView(@NotNull XDebugSession debugSession, @NotNull InstancesTree tree, + @NotNull StackFrameList list, String className) { + mySplitter.setFirstComponent(new JBScrollPane(tree)); + + list.setEmptyText(EMPTY_TEXT_WHEN_ITEM_NOT_SELECTED); + JLabel stackTraceLabel; + if (isArrayType(className)) { + stackTraceLabel = new JBLabel(TEXT_FOR_ARRAYS, SwingConstants.CENTER); + } else { + ActionLink actionLink = new ActionLink("Enable tracking for new instances", MemoryViewIcons.CLASS_TRACKED, new AnAction() { + @Override + public void actionPerformed(AnActionEvent e) { + InstancesTracker.getInstance(debugSession.getProject()).add(className, TrackingType.CREATION); + } + }); + + actionLink.setHorizontalAlignment(SwingConstants.CENTER); + actionLink.setPaintUnderline(false); + stackTraceLabel = actionLink; + } + + mySplitter.setSplitterProportionKey("InstancesWithStackFrameView.SplitterKey"); + + JComponent stackComponent = new JBScrollPane(list); + + CreationPositionTracker tracker = CreationPositionTracker.getInstance(debugSession.getProject()); + InstancesTracker instancesTracker = InstancesTracker.getInstance(debugSession.getProject()); + instancesTracker.addTrackerListener(new InstancesTrackerListener() { + @Override + public void classChanged(@NotNull String name, @NotNull TrackingType type) { + if (Objects.equals(className, name) && type == TrackingType.CREATION) { + mySplitter.setSecondComponent(stackComponent); + } + } + + @Override + public void classRemoved(@NotNull String name) { + if (Objects.equals(name, className)) { + mySplitter.setSecondComponent(stackTraceLabel); + } + } + }, tree); + + mySplitter.setSecondComponent(instancesTracker.isTracked(className) ? stackComponent : stackTraceLabel); + + mySplitter.setHonorComponentsMinimumSize(false); + myHidedProportion = DEFAULT_SPLITTER_PROPORTION; + + tree.addTreeSelectionListener(e -> { + ObjectReference ref = tree.getSelectedReference(); + if (ref != null && tracker != null) { + List stack = tracker.getStack(debugSession, ref); + if (stack != null) { + list.setFrame(stack); + if(mySplitter.getProportion() == 1.f) { + mySplitter.setProportion(DEFAULT_SPLITTER_PROPORTION); + } + return; + } + list.setEmptyText(EMPTY_TEXT_WHEN_STACK_NOT_FOUND); + } else { + list.setEmptyText(EMPTY_TEXT_WHEN_ITEM_NOT_SELECTED); + } + + list.setFrame(EMPTY_FRAME); + }); + } + + JComponent getComponent() { + return mySplitter; + } + + private static boolean isArrayType(@NotNull String className) { + return className.contains("[]"); + } + + @SuppressWarnings("unused") + private void hideStackFrame() { + if (!myIsHided) { + myHidedProportion = mySplitter.getProportion(); + mySplitter.getSecondComponent().setVisible(false); + mySplitter.setProportion(1.f); + myIsHided = true; + } + } + + @SuppressWarnings("unused") + private void showStackFrame() { + if (myIsHided) { + mySplitter.getSecondComponent().setVisible(true); + mySplitter.setProportion(myHidedProportion); + myIsHided = false; + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFrameList.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFrameList.java new file mode 100644 index 000000000000..ce9431591901 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFrameList.java @@ -0,0 +1,141 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.debugger.engine.DebuggerUtils; +import com.intellij.execution.filters.OpenFileHyperlinkInfo; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.components.JBList; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; + +class StackFrameList extends JBList { + private static final char ANONYMOUS_CLASS_DELIMITER = '$'; + private static final MyOpenFilesState myEditorState = new MyOpenFilesState(); + + private List myStackFrames; + private final Project myProject; + private final GlobalSearchScope myScope; + private final MyListModel myModel = new MyListModel(); + + StackFrameList(@NotNull Project project, + @NotNull List stack, + @NotNull GlobalSearchScope searchScope) { + super(); + + myStackFrames = new ArrayList<>(stack); + myProject = project; + myScope = searchScope; + + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + //noinspection unchecked + setModel(myModel); + + setCellRenderer(new ColoredListCellRenderer() { + @Override + protected void customizeCellRenderer(@NotNull JList list, + StackFrameDescriptor value, int index, boolean isSelected, boolean hasFocus) { + append(String.format("%s:%d, %s", value.methodName(), value.line(), value.className())); + String packageName = value.packageName(); + if (!StringUtils.isEmpty(packageName)) { + append(String.format(" (%s)", value.packageName()), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES); + } + } + }); + } + + void setFrame(@NotNull List stack) { + myModel.update(stack); + } + + void navigateToSelectedValue(boolean focusOnEditor) { + StackFrameDescriptor selectedValue = (StackFrameDescriptor) getSelectedValue(); + if (selectedValue != null) { + navigateToFrame(selectedValue, focusOnEditor); + } + } + + private void navigateToFrame(@NotNull StackFrameDescriptor frame, boolean focusOnEditor) { + String path = frame.path(); + int anonymousClassDelimiterIndex = path.indexOf(ANONYMOUS_CLASS_DELIMITER); + int pathLength = anonymousClassDelimiterIndex > 0 ? anonymousClassDelimiterIndex : path.length(); + path = path.substring(0, pathLength); + PsiClass psiClass = DebuggerUtils.findClass(path, myProject, myScope); + if (psiClass != null) { + ApplicationManager.getApplication().runReadAction(() -> { + PsiElement navigationElement = psiClass.getNavigationElement(); + VirtualFile file = PsiUtilCore.getVirtualFile(navigationElement); + + if (file == null) { + file = psiClass.getContainingFile().getVirtualFile(); + } + + OpenFileHyperlinkInfo info = + new OpenFileHyperlinkInfo(myProject, file, frame.line() - 1); + OpenFileDescriptor descriptor = info.getDescriptor(); + if (descriptor != null) { + FileEditorManagerImpl manager = (FileEditorManagerImpl)FileEditorManager.getInstance(myProject); + VirtualFile lastFile = myEditorState.myLastOpenedFile; + if (myEditorState.myIsNeedToCloseLastOpenedFile && lastFile != null && + manager.isFileOpen(lastFile) && !lastFile.equals(descriptor.getFile())) { + manager.closeFile(myEditorState.myLastOpenedFile, false, true); + } + + descriptor.setScrollType(ScrollType.CENTER); + descriptor.setUseCurrentWindow(true); + + if (lastFile == null || !lastFile.equals(descriptor.getFile())) { + myEditorState.myIsNeedToCloseLastOpenedFile = !manager.isFileOpen(descriptor.getFile()); + } + + descriptor.navigateInEditor(myProject, focusOnEditor); + FileEditor[] editors = manager.getEditors(descriptor.getFile()); + if (editors.length != 0) { + myEditorState.myLastOpenedFile = descriptor.getFile(); + } + } + }); + } + } + + private static class MyOpenFilesState { + VirtualFile myLastOpenedFile; + boolean myIsNeedToCloseLastOpenedFile; + } + + private class MyListModel extends AbstractListModel { + + void update(@NotNull List newFrame) { + fireIntervalRemoved(this, 0, getSize()); + myStackFrames = newFrame; + fireIntervalAdded(this, 0, getSize()); + } + + @Override + public int getSize() { + return myStackFrames.size(); + } + + @Override + public StackFrameDescriptor getElementAt(int index) { + return myStackFrames.get(index); + } + } +} diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFramePopup.java b/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFramePopup.java new file mode 100644 index 000000000000..0069d0500152 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/memory/view/StackFramePopup.java @@ -0,0 +1,43 @@ +package org.jetbrains.debugger.memory.view; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.psi.search.GlobalSearchScope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.memory.utils.StackFrameDescriptor; + +import java.util.List; + +public class StackFramePopup { + private final Project myProject; + private final List myStackFrame; + private final GlobalSearchScope myScope; + + public StackFramePopup(@NotNull Project project, + @NotNull List stack, + @NotNull GlobalSearchScope searchScope) { + myProject = project; + myStackFrame = stack; + myScope = searchScope; + } + + public void show() { + StackFrameList list = new StackFrameList(myProject, myStackFrame, myScope); + list.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + list.navigateToSelectedValue(false); + } + }); + + JBPopup popup = JBPopupFactory.getInstance().createListPopupBuilder(list) + .setTitle("Select stack frame") + .setAutoSelectIfEmpty(true) + .setResizable(false) + .setItemChoosenCallback(() -> list.navigateToSelectedValue(true)) + .createPopup(); + + list.setSelectedIndex(1); + popup.showInFocusCenter(); + } +}