Move Memory view repository to IDEA repo

This commit is contained in:
Vitaliy.Bibaev
2016-12-14 21:20:22 +03:00
52 changed files with 4353 additions and 0 deletions
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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<ReferenceType> referenceTypes = vm.classesByName(elementTypeName);
if (referenceTypes.size() == 1) {
return referenceTypes.get(0);
}
return null;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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();
}
}
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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);
}
}
}
@@ -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<StackFrameDescriptor> 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<StackFrameDescriptor> 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);
}
}
@@ -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);
}
}
@@ -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<XDebugSession, Map<ObjectReference, List<StackFrameDescriptor>>>
mySession2Reference2Stack = new ConcurrentHashMap<>();
private final ConcurrentHashMap<XDebugSession, Map<ObjectReference, List<StackFrameDescriptor>>>
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<StackFrameDescriptor> getStack(@NotNull XDebugSession session, @NotNull ObjectReference ref) {
List<StackFrameDescriptor> 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<ObjectReference, List<StackFrameDescriptor>> ref2Stack = myPinnedSession2Reference2Stack.getOrDefault(session, null);
if (ref2Stack != null) {
Iterator<ObjectReference> 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<ObjectReference, List<StackFrameDescriptor>> ref2Stack = mySession2Reference2Stack.getOrDefault(session, null);
if (ref2Stack != null) {
Map<ObjectReference, List<StackFrameDescriptor>> 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> T extract(@NotNull Map<XDebugSession, Map<ObjectReference, T>> map,
@NotNull XDebugSession session, @NotNull ObjectReference ref) {
Map<ObjectReference, T> ref2something = map.getOrDefault(session, null);
if(ref2something != null) {
return ref2something.getOrDefault(ref, null);
}
return null;
}
}
@@ -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<InstancesTracker.MyState> {
private final EventDispatcher<InstancesTrackerListener> 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<String, TrackingType> 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<String, TrackingType> classes = new ConcurrentHashMap<>();
MyState() {
}
MyState(@NotNull MyState state) {
isBackgroundTrackingEnabled = state.isBackgroundTrackingEnabled;
for (Map.Entry<String, TrackingType> classState : state.classes.entrySet()) {
classes.put(classState.getKey(), classState.getValue());
}
}
}
}
@@ -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<MemoryViewManagerState> {
private final EventDispatcher<MemoryViewManagerListener> 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));
}
}
@@ -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;
}
}
@@ -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) {
}
}
@@ -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);
}
@@ -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<ObjectReference> 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<ObjectReference> 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<ObjectReference> 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;
}
}
@@ -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<ObjectReference> visited, boolean isRoot);
}
@@ -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<XDebugSession, ClassesFilteredView> 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<Project> {
@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);
}
}
}
}
}
@@ -0,0 +1,5 @@
package org.jetbrains.debugger.memory.tracking;
interface BackgroundTracker {
void setBackgroundMode(boolean isBackgroundMode);
}
@@ -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);
}
}
}
@@ -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<ObjectReference> myNewObjects = null;
@NotNull
private HashSet<ObjectReference> 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<ObjectReference> 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<JavaLineBreakpointProperties>
implements Disposable {
private boolean myIsEnabled = false;
private final List<BreakpointRequest> 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<StackFrameProxyImpl> stack = threadReferenceProxy == null ? null : threadReferenceProxy.frames();
if (stack != null) {
List<StackFrameDescriptor> 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;
}
}
}
}
@@ -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<Integer> myHashesSet;
private MyState myState = MyState.WAIT_HASHES;
private int myCount = 0;
HashBasedTracking(@Nullable SuspendContextImpl suspendContext,
@NotNull ReferenceType classType,
@NotNull List<ObjectReference> initialInstances) {
myClassType = classType;
if (suspendContext != null) {
myHashesSet = toSetOfHashes(evalHashCodes(suspendContext, initialInstances));
myState = myState.next();
}
}
@NotNull
@Override
protected List<ObjectReference> updateImpl(@NotNull SuspendContextImpl suspendContext,
@NotNull List<ObjectReference> references) {
Map<ObjectReference, Optional<Integer>> ref2hash = evalHashCodes(suspendContext, references);
List<ObjectReference> newInstances = new ArrayList<>();
for (ObjectReference ref : references) {
Optional<Integer> 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<ObjectReference, Optional<Integer>> evalHashCodes(@NotNull SuspendContextImpl suspendContext,
@NotNull List<ObjectReference> references) {
MyHashCodeMethodWrapper hashCodeCaller = getHashCodeMethod(myClassType);
Map<ObjectReference, Optional<Integer>> 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<Integer> 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<Integer> toSetOfHashes(@NotNull Map<ObjectReference, Optional<Integer>> 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<ReferenceType> 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<Integer> eval(@NotNull ObjectReference ref, @NotNull SuspendContextImpl suspendContext,
@NotNull ThreadReference thread) throws InvocationException, InvalidTypeException,
ClassNotLoadedException, IncompatibleThreadStateException;
}
}
@@ -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<ObjectReference> initialInstances) {
myLastId = getMaxId(initialInstances);
}
@NotNull
@Override
protected List<ObjectReference> updateImpl(@NotNull SuspendContextImpl suspendContext,
@NotNull List<ObjectReference> references) {
List<ObjectReference> 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<ObjectReference> references) {
return references.stream().map(ObjectReference::uniqueID).max(Long::compare).orElse(-1L);
}
@Override
public int getCount() {
return myCount;
}
}
@@ -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<ObjectReference> myNewInstances = new ArrayList<>();
private boolean myIsReady = false;
public static InstanceTrackingStrategy create(@NotNull ReferenceType referenceType,
@Nullable SuspendContextImpl suspendContext,
@NotNull TrackingType type,
@NotNull List<ObjectReference> 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<ObjectReference> getNewInstances() {
return Collections.unmodifiableList(myNewInstances);
}
public final void update(@NotNull SuspendContextImpl suspendContext, @NotNull List<ObjectReference> references) {
myNewInstances = updateImpl(suspendContext, references);
myIsReady = true;
}
@Override
public boolean isReady() {
return myIsReady;
}
@NotNull
protected abstract List<ObjectReference> updateImpl(@NotNull SuspendContextImpl suspendContext,
@NotNull List<ObjectReference> references);
}
@@ -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<ObjectReference> myHardRefs;
RetainReferencesTracking(@NotNull List<ObjectReference> initialInstances) {
myHardRefs = initialInstances;
myIdTracker = new IdentityBasedTracking(initialInstances);
}
@NotNull
@Override
protected List<ObjectReference> updateImpl(@NotNull SuspendContextImpl suspendContext,
@NotNull List<ObjectReference> references) {
myIdTracker.update(suspendContext, references);
myHardRefs = references;
return myIdTracker.getNewInstances();
}
@Override
public int getCount() {
return myHardRefs.size();
}
}
@@ -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<ObjectReference> getNewInstances();
int getCount();
boolean isReady();
}
@@ -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; }
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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<String, List<XNamedValue>> myErrorMessage2ValueMap = new HashMap<>();
public ErrorsValueGroup(@NotNull String name) {
super(name);
}
public void addErrorValue(@NotNull String message, @NotNull XNamedValue value) {
List<XNamedValue> 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);
}
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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<ObjectReference> getInstances(int limit);
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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<ReferenceType, ConstructorInstancesTracker> myConstructorTrackedClasses = new ConcurrentHashMap<>();
private final XDebugSessionListener myDebugSessionListener;
@Nullable
private volatile SuspendContextImpl myLastSuspendContext;
/**
* Indicates that the debug session had been stopped at least once.
* <p>
* 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<String, TrackingType> entry : myInstancesTracker.getTrackedClasses().entrySet()) {
TrackingType type = entry.getValue();
String className = entry.getKey();
List<ReferenceType> 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<ReferenceType> 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<long[]> 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<ReferenceType> 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<ObjectReference> 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;
}
}
@@ -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<ReferenceType> SELECTED_CLASS_KEY = DataKey.create("ClassesTable.SelectedClass");
public static final DataKey<XDebugSession> DEBUG_SESSION_KEY = DataKey.create("ClassesTable.DebugSession");
public static final DataKey<InstancesProvider> NEW_INSTANCES_PROVIDER_KEY =
DataKey.create("ClassesTable.NewInstances");
public static final DataKey<ReferenceCountProvider> 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<ReferenceType, DiffValue> 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<ReferenceType> 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<DiffViewTableModel> sorter = new TableRowSorter<>(myModel);
sorter.setRowFilter(new RowFilter<DiffViewTableModel, Integer>() {
@Override
public boolean include(Entry<? extends DiffViewTableModel, ? extends Integer> 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<RowSorter.SortKey> 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<ReferenceType> 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<ObjectReference> 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
* <p>
* 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<DiffValue> {
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<TextRange> 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);
}
}
}
}
@@ -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<Void, Void>() {
@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<XExpression> expressions = getRecentExpressions();
if (!expressions.isEmpty()) {
ListPopupImpl historyPopup = new ListPopupImpl(new BaseListPopupStep<XExpression>(null, expressions) {
@Override
public PopupStep onChosen(XExpression selectedValue, boolean finalChoice) {
setExpression(selectedValue);
requestFocusInEditor();
return FINAL_CHOICE;
}
}) {
@Override
protected ListCellRenderer getListElementRenderer() {
return new ColoredListCellRenderer<XExpression>() {
@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());
}
}
}
@@ -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("<html>%s<br>%s</html>", itemsInfo, errors);
}
return itemsInfo;
}
}
@@ -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<XDebugSession> DEBUG_SESSION_DATA_KEY = DataKey.create("InstancesTree.DebugSession");
private final XValueNodeImpl myRoot;
private final Runnable myOnRootExpandAction;
private final XDebugSession myDebugSession;
private List<XValueChildrenList> 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);
}
}
}
@@ -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<ObjectReference> 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<Void, Void> {
private final List<ObjectReference> 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<ObjectReference> 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<MyFilteringResult, String> 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<MyFilteringResult, String> 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
}
}
@@ -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<StackFrameDescriptor> 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<StackFrameDescriptor> 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;
}
}
}
@@ -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<StackFrameDescriptor> myStackFrames;
private final Project myProject;
private final GlobalSearchScope myScope;
private final MyListModel myModel = new MyListModel();
StackFrameList(@NotNull Project project,
@NotNull List<StackFrameDescriptor> stack,
@NotNull GlobalSearchScope searchScope) {
super();
myStackFrames = new ArrayList<>(stack);
myProject = project;
myScope = searchScope;
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
//noinspection unchecked
setModel(myModel);
setCellRenderer(new ColoredListCellRenderer<StackFrameDescriptor>() {
@Override
protected void customizeCellRenderer(@NotNull JList<? extends StackFrameDescriptor> 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<StackFrameDescriptor> 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<StackFrameDescriptor> {
void update(@NotNull List<StackFrameDescriptor> 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);
}
}
}
@@ -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<StackFrameDescriptor> myStackFrame;
private final GlobalSearchScope myScope;
public StackFramePopup(@NotNull Project project,
@NotNull List<StackFrameDescriptor> 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();
}
}