/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.debugger.memory.ui; 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.EvaluationContext; import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; import com.intellij.debugger.engine.events.DebuggerContextCommandImpl; import com.intellij.debugger.memory.filtering.FilteringResult; import com.intellij.debugger.memory.filtering.FilteringTask; import com.intellij.debugger.memory.filtering.FilteringTaskCallback; import com.intellij.debugger.memory.utils.*; 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.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.AnActionListener; import com.intellij.openapi.application.ApplicationManager; 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.wm.IdeFocusManager; import com.intellij.ui.DoubleClickListener; import com.intellij.ui.JBColor; 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.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.ObjectReference; import com.sun.jdi.Value; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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.List; import java.util.concurrent.TimeUnit; 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; private final Project myProject; private final DebugProcessImpl myDebugProcess; private final InstancesProvider myInstancesProvider; private final String myClassName; private final MyInstancesView myInstancesView; public InstancesWindow(@NotNull XDebugSession session, @NotNull InstancesProvider provider, @NotNull String className) { super(session.getProject(), false); myProject = session.getProject(); myDebugProcess = (DebugProcessImpl)DebuggerManager.getInstance(myProject) .getDebugProcess(session.getDebugProcess().getProcessHandler()); myInstancesProvider = provider; myClassName = className; addWarningMessage(null); session.addSessionListener(new XDebugSessionListener() { @Override public void sessionStopped() { ApplicationManager.getApplication().invokeLater(() -> close(OK_EXIT_CODE)); } }, myDisposable); setModal(false); myInstancesView = new MyInstancesView(session); myInstancesView.setPreferredSize( new JBDimension(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)); init(); JRootPane root = myInstancesView.getRootPane(); root.setDefaultButton(myInstancesView.myFilterButton); } 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() { 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 = 50; private static final int MAX_DURATION_TO_UPDATE_TREE_SECONDS = 3; private static final int FILTERING_PROGRESS_UPDATING_MIN_DELAY_MILLIS = 17; // ~ 60 fps private final InstancesTree myInstancesTree; private final XDebuggerExpressionEditor myFilterConditionEditor; private final XDebugSessionListener myDebugSessionListener = new MySessionListener(); private final MyNodeManager myNodeManager = new MyNodeManager(myProject); private final JButton myFilterButton = new JButton("Filter"); private final FilteringProgressView myProgress = new FilteringProgressView(); private final Object myFilteringTaskLock = new Object(); private boolean myIsAndroidVM = false; private volatile MyFilteringWorker myFilteringTask = null; MyInstancesView(@NotNull XDebugSession session) { super(new BorderLayout(0, JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP))); Disposer.register(InstancesWindow.this.myDisposable, this); final XValueMarkers<?, ?> markers = getValueMarkers(session); if (markers != null) { final MyActionListener listener = new MyActionListener(markers); ActionManager.getInstance().addAnActionListener(listener, InstancesWindow.this.myDisposable); } session.addSessionListener(myDebugSessionListener, InstancesWindow.this.myDisposable); final JavaDebuggerEditorsProvider editorsProvider = new JavaDebuggerEditorsProvider(); myFilterConditionEditor = new ExpressionEditorWithHistory(myProject, myClassName, editorsProvider, InstancesWindow.this.myDisposable); myFilterButton.setBorder(BorderFactory.createEmptyBorder()); final Dimension filteringButtonSize = myFilterConditionEditor.getEditorComponent().getPreferredSize(); filteringButtonSize.width = JBUI.scale(FILTERING_BUTTON_ADDITIONAL_WIDTH) + myFilterButton.getPreferredSize().width; myFilterButton.setPreferredSize(filteringButtonSize); final JBPanel filteringPane = new JBPanel(new BorderLayout(JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP), 0)); final 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, editorsProvider, markers, this::updateInstances); myFilterButton.addActionListener(e -> { final String expression = myFilterConditionEditor.getExpression().getExpression(); if (!expression.isEmpty()) { myFilterConditionEditor.saveTextInHistory(); } myFilterButton.setEnabled(false); myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES); }); final StackFrameList list = new StackFrameList(myDebugProcess); list.addListSelectionListener(e -> list.navigateToSelectedValue(false)); new DoubleClickListener() { @Override protected boolean onDoubleClick(MouseEvent event) { list.navigateToSelectedValue(true); return true; } }.installOn(list); final InstancesWithStackFrameView instancesWithStackFrame = new InstancesWithStackFrameView(session, myInstancesTree, list, myClassName); add(filteringPane, BorderLayout.NORTH); add(instancesWithStackFrame.getComponent(), BorderLayout.CENTER); final JComponent focusedComponent = myFilterConditionEditor.getEditorComponent(); UiNotifyConnector.doWhenFirstShown(focusedComponent, () -> IdeFocusManager.findInstanceByComponent(focusedComponent) .requestFocus(focusedComponent, true)); } @Override public void dispose() { cancelFilteringTask(); Disposer.dispose(myInstancesTree); } private void updateInstances() { cancelFilteringTask(); myDebugProcess.getManagerThread().schedule(new DebuggerContextCommandImpl(myDebugProcess.getDebuggerContext()) { @Override public Priority getPriority() { return Priority.LOWEST; } @Override public void threadAction(@NotNull SuspendContextImpl suspendContext) { myIsAndroidVM = AndroidUtil.isAndroidVM(myDebugProcess.getVirtualMachineProxy().getVirtualMachine()); final int limit = myIsAndroidVM ? AndroidUtil.ANDROID_INSTANCES_LIMIT : DEFAULT_INSTANCES_LIMIT; List<ObjectReference> instances = myInstancesProvider.getInstances(limit + 1); final EvaluationContextImpl evaluationContext = myDebugProcess .getDebuggerContext().createEvaluationContext(); if (instances.size() > limit) { addWarningMessage(String.format("Not all instances will be loaded (only %d)", limit)); instances = instances.subList(0, limit); } if (evaluationContext != null) { synchronized (myFilteringTaskLock) { myFilteringTask = new MyFilteringWorker(instances, myFilterConditionEditor.getExpression(), evaluationContext); myFilteringTask.execute(); } } } }); } private void cancelFilteringTask() { if (myFilteringTask != null) { synchronized (myFilteringTaskLock) { if (myFilteringTask != null) { myFilteringTask.cancel(); myFilteringTask = null; } } } } private XValueMarkers<?, ?> getValueMarkers(@NotNull XDebugSession session) { return session instanceof XDebugSessionImpl ? ((XDebugSessionImpl)session).getValueMarkers() : null; } private class MySessionListener implements XDebugSessionListener { private volatile XDebuggerTreeState myTreeState = null; @Override public void sessionResumed() { ApplicationManager.getApplication().invokeLater(() -> { myTreeState = XDebuggerTreeState.saveState(myInstancesTree); cancelFilteringTask(); myInstancesTree.setInfoMessage( "The application is running"); }); } @Override public void sessionPaused() { ApplicationManager.getApplication().invokeLater(() -> { myProgress.setVisible(true); myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES, myTreeState); }); } } private class MyActionListener extends AnActionListener.Adapter { private final XValueMarkers<?, ?> myValueMarkers; private MyActionListener(@NotNull XValueMarkers<?, ?> markers) { myValueMarkers = markers; } @Override public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { if (dataContext.getData(PlatformDataKeys.CONTEXT_COMPONENT) == myInstancesTree && (isAddToWatchesAction(action) || isEvaluateExpressionAction(action))) { XValueNodeImpl selectedNode = XDebuggerTreeActionBase.getSelectedNode(dataContext); if (selectedNode != null) { TreeNode currentNode = selectedNode; while (!myInstancesTree.getRoot().equals(currentNode.getParent())) { currentNode = currentNode.getParent(); } final XValue valueContainer = ((XValueNodeImpl)currentNode).getValueContainer(); final String expression = valueContainer.getEvaluationExpression(); if (expression != null) { myValueMarkers.markValue(valueContainer, new ValueMarkup(expression.replace("@", ""), new JBColor(0, 0), null)); } ApplicationManager.getApplication().invokeLater(() -> myInstancesTree .rebuildTree(InstancesTree.RebuildPolicy.ONLY_UPDATE_LABELS)); } } } private boolean isAddToWatchesAction(AnAction action) { final String className = action.getClass().getSimpleName(); return action instanceof XDebuggerTreeActionBase && className.equals("XAddToWatchesAction"); } private boolean isEvaluateExpressionAction(AnAction action) { final String className = action.getClass().getSimpleName(); return action instanceof XDebuggerActionBase && className.equals("EvaluateAction"); } } private class MyFilteringCallback implements FilteringTaskCallback { private final ErrorsValueGroup myErrorsGroup = new ErrorsValueGroup(); private final EvaluationContextImpl myEvaluationContext; private long myFilteringStartedTime; private int myProceedCount = 0; private int myMatchedCount = 0; private int myErrorsCount = 0; private long myLastTreeUpdatingTime; private long myLastProgressUpdatingTime; public MyFilteringCallback(@NotNull EvaluationContextImpl evaluationContext) { myEvaluationContext = evaluationContext; } private XValueChildrenList myChildren = new XValueChildrenList(); @Override public void started(int total) { myFilteringStartedTime = System.nanoTime(); myLastTreeUpdatingTime = myFilteringStartedTime; myLastProgressUpdatingTime = System.nanoTime(); ApplicationManager.getApplication().invokeLater(() -> myProgress.start(total)); } @NotNull @Override public Action matched(@NotNull Value ref) { final JavaValue val = new InstanceJavaValue(new InstanceValueDescriptor(myProject, ref), myEvaluationContext, myNodeManager); myMatchedCount++; myProceedCount++; myChildren.add(val); updateProgress(); updateTree(); return myMatchedCount < MAX_TREE_NODE_COUNT ? Action.CONTINUE : Action.STOP; } @NotNull @Override public Action notMatched(@NotNull Value ref) { myProceedCount++; updateProgress(); return Action.CONTINUE; } @NotNull @Override public Action error(@NotNull Value ref, @NotNull String description) { final JavaValue val = new InstanceJavaValue(new InstanceValueDescriptor(myProject, ref), myEvaluationContext, myNodeManager); myErrorsGroup.addErrorValue(description, val); myProceedCount++; myErrorsCount++; updateProgress(); return Action.CONTINUE; } @Override public void completed(@NotNull FilteringResult reason) { if (!myErrorsGroup.isEmpty()) { myChildren.addBottomGroup(myErrorsGroup); } final long duration = System.nanoTime() - myFilteringStartedTime; LOG.info(String.format("Filtering completed in %d ms for %d instances", TimeUnit.NANOSECONDS.toMillis(duration), myProceedCount)); final int proceed = myProceedCount; final int matched = myMatchedCount; final int errors = myErrorsCount; final XValueChildrenList childrenList = myChildren; ApplicationManager.getApplication().invokeLater(() -> { myProgress.updateProgress(proceed, matched, errors); myInstancesTree.addChildren(childrenList, true); myFilterButton.setEnabled(true); myProgress.complete(reason); }); } private void updateProgress() { final long now = System.nanoTime(); if (now - myLastProgressUpdatingTime > TimeUnit.MILLISECONDS.toNanos(FILTERING_PROGRESS_UPDATING_MIN_DELAY_MILLIS)) { final int proceed = myProceedCount; final int matched = myMatchedCount; final int errors = myErrorsCount; ApplicationManager.getApplication().invokeLater(() -> myProgress.updateProgress(proceed, matched, errors)); myLastProgressUpdatingTime = now; } } private void updateTree() { final long now = System.nanoTime(); final int newChildrenCount = myChildren.size(); if (newChildrenCount >= FILTERING_CHUNK_SIZE || (newChildrenCount > 0 && now - myLastTreeUpdatingTime > TimeUnit.SECONDS.toNanos(MAX_DURATION_TO_UPDATE_TREE_SECONDS))) { final XValueChildrenList children = myChildren; ApplicationManager.getApplication().invokeLater(() -> myInstancesTree.addChildren(children, false)); myChildren = new XValueChildrenList(); myLastTreeUpdatingTime = System.nanoTime(); } } } private class MyFilteringWorker extends SwingWorker<Void, Void> { private final FilteringTask myTask; MyFilteringWorker(@NotNull List<ObjectReference> refs, @NotNull XExpression expression, @NotNull EvaluationContextImpl evaluationContext) { myTask = new FilteringTask(myClassName, myDebugProcess, expression, new MyValuesList(refs), new MyFilteringCallback(evaluationContext)); } @Override protected Void doInBackground() throws Exception { try { myTask.run(); } catch (Throwable e) { LOG.error(e); } return null; } public void cancel() { myTask.cancel(); super.cancel(false); } } } private static class MyValuesList implements FilteringTask.ValuesList { private final List<ObjectReference> myRefs; public MyValuesList(List<ObjectReference> refs) { myRefs = refs; } @Override public int size() { return myRefs.size(); } @Override public ObjectReference get(int index) { return myRefs.get(index); } } private final static class MyNodeManager extends NodeManagerImpl { MyNodeManager(Project project) { super(project, null); } @NotNull @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); } @NotNull @Override public DebuggerTreeNodeImpl createMessageNode(String message) { return new DebuggerTreeNodeImpl(null, new MessageDescriptor(message)); } } }