Move AndroidUtil.isAndroidVM into DebuggerUtils
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / memory / ui / InstancesWindow.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.debugger.memory.ui;
17
18 import com.intellij.debugger.DebuggerManager;
19 import com.intellij.debugger.engine.DebugProcessImpl;
20 import com.intellij.debugger.engine.DebuggerUtils;
21 import com.intellij.debugger.engine.JavaValue;
22 import com.intellij.debugger.engine.SuspendContextImpl;
23 import com.intellij.debugger.engine.evaluation.EvaluationContext;
24 import com.intellij.debugger.engine.evaluation.EvaluationContextImpl;
25 import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
26 import com.intellij.debugger.memory.filtering.FilteringResult;
27 import com.intellij.debugger.memory.filtering.FilteringTask;
28 import com.intellij.debugger.memory.filtering.FilteringTaskCallback;
29 import com.intellij.debugger.memory.utils.*;
30 import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl;
31 import com.intellij.debugger.ui.impl.watch.MessageDescriptor;
32 import com.intellij.debugger.ui.impl.watch.NodeManagerImpl;
33 import com.intellij.debugger.ui.tree.NodeDescriptor;
34 import com.intellij.openapi.Disposable;
35 import com.intellij.openapi.actionSystem.*;
36 import com.intellij.openapi.actionSystem.ex.AnActionListener;
37 import com.intellij.openapi.application.ApplicationManager;
38 import com.intellij.openapi.diagnostic.Logger;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.ui.DialogWrapper;
41 import com.intellij.openapi.util.Disposer;
42 import com.intellij.openapi.wm.IdeFocusManager;
43 import com.intellij.ui.DoubleClickListener;
44 import com.intellij.ui.JBColor;
45 import com.intellij.ui.components.JBLabel;
46 import com.intellij.ui.components.JBPanel;
47 import com.intellij.util.ui.JBDimension;
48 import com.intellij.util.ui.JBUI;
49 import com.intellij.util.ui.UIUtil;
50 import com.intellij.util.ui.update.UiNotifyConnector;
51 import com.intellij.xdebugger.XDebugSession;
52 import com.intellij.xdebugger.XDebugSessionListener;
53 import com.intellij.xdebugger.XExpression;
54 import com.intellij.xdebugger.frame.XValue;
55 import com.intellij.xdebugger.frame.XValueChildrenList;
56 import com.intellij.xdebugger.impl.XDebugSessionImpl;
57 import com.intellij.xdebugger.impl.actions.XDebuggerActionBase;
58 import com.intellij.xdebugger.impl.frame.XValueMarkers;
59 import com.intellij.xdebugger.impl.ui.XDebuggerExpressionEditor;
60 import com.intellij.xdebugger.impl.ui.tree.ValueMarkup;
61 import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState;
62 import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase;
63 import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl;
64 import com.sun.jdi.ObjectReference;
65 import com.sun.jdi.Value;
66 import org.jetbrains.annotations.NotNull;
67 import org.jetbrains.annotations.Nullable;
68 import org.jetbrains.java.debugger.JavaDebuggerEditorsProvider;
69
70 import javax.swing.*;
71 import javax.swing.tree.TreeNode;
72 import java.awt.*;
73 import java.awt.event.MouseEvent;
74 import java.util.List;
75 import java.util.concurrent.TimeUnit;
76
77 public class InstancesWindow extends DialogWrapper {
78   private static final Logger LOG = Logger.getInstance(InstancesWindow.class);
79
80   private static final int DEFAULT_WINDOW_WIDTH = 870;
81   private static final int DEFAULT_WINDOW_HEIGHT = 400;
82   private static final int FILTERING_BUTTON_ADDITIONAL_WIDTH = 30;
83   private static final int BORDER_LAYOUT_DEFAULT_GAP = 5;
84   private static final int DEFAULT_INSTANCES_LIMIT = 500000;
85
86   private final Project myProject;
87   private final DebugProcessImpl myDebugProcess;
88   private final InstancesProvider myInstancesProvider;
89   private final String myClassName;
90   private final MyInstancesView myInstancesView;
91
92   public InstancesWindow(@NotNull XDebugSession session,
93                          @NotNull InstancesProvider provider,
94                          @NotNull String className) {
95     super(session.getProject(), false);
96
97     myProject = session.getProject();
98     myDebugProcess = (DebugProcessImpl)DebuggerManager.getInstance(myProject)
99       .getDebugProcess(session.getDebugProcess().getProcessHandler());
100     myInstancesProvider = provider;
101     myClassName = className;
102
103     addWarningMessage(null);
104     session.addSessionListener(new XDebugSessionListener() {
105       @Override
106       public void sessionStopped() {
107         ApplicationManager.getApplication().invokeLater(() -> close(OK_EXIT_CODE));
108       }
109     }, myDisposable);
110     setModal(false);
111     myInstancesView = new MyInstancesView(session);
112     myInstancesView.setPreferredSize(
113       new JBDimension(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT));
114
115     init();
116
117     JRootPane root = myInstancesView.getRootPane();
118     root.setDefaultButton(myInstancesView.myFilterButton);
119   }
120
121   private void addWarningMessage(@Nullable String message) {
122     String warning = message == null ? "" : String.format(". Warning: %s", message);
123     setTitle(String.format("Instances of %s%s", myClassName, warning));
124   }
125
126   @NotNull
127   @Override
128   protected String getDimensionServiceKey() {
129     return "#org.jetbrains.debugger.memory.view.InstancesWindow";
130   }
131
132   @Nullable
133   @Override
134   protected JComponent createCenterPanel() {
135     return myInstancesView;
136   }
137
138   @Nullable
139   @Override
140   protected JComponent createSouthPanel() {
141     JComponent comp = super.createSouthPanel();
142     if (comp != null) {
143       comp.add(myInstancesView.myProgress, BorderLayout.WEST);
144     }
145
146     return comp;
147   }
148
149   @NotNull
150   @Override
151   protected Action[] createActions() {
152     return new Action[]{new DialogWrapperExitAction("Close", CLOSE_EXIT_CODE)};
153   }
154
155   private class MyInstancesView extends JBPanel implements Disposable {
156     private static final int MAX_TREE_NODE_COUNT = 2000;
157     private static final int FILTERING_CHUNK_SIZE = 50;
158
159     private static final int MAX_DURATION_TO_UPDATE_TREE_SECONDS = 3;
160     private static final int FILTERING_PROGRESS_UPDATING_MIN_DELAY_MILLIS = 17; // ~ 60 fps
161
162     private final InstancesTree myInstancesTree;
163     private final XDebuggerExpressionEditor myFilterConditionEditor;
164     private final XDebugSessionListener myDebugSessionListener = new MySessionListener();
165
166     private final MyNodeManager myNodeManager = new MyNodeManager(myProject);
167
168     private final JButton myFilterButton = new JButton("Filter");
169     private final FilteringProgressView myProgress = new FilteringProgressView();
170
171     private final Object myFilteringTaskLock = new Object();
172
173     private boolean myIsAndroidVM = false;
174
175     private volatile MyFilteringWorker myFilteringTask = null;
176
177     MyInstancesView(@NotNull XDebugSession session) {
178       super(new BorderLayout(0, JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP)));
179
180       Disposer.register(InstancesWindow.this.myDisposable, this);
181       final XValueMarkers<?, ?> markers = getValueMarkers(session);
182       if (markers != null) {
183         final MyActionListener listener = new MyActionListener(markers);
184         ActionManager.getInstance().addAnActionListener(listener, InstancesWindow.this.myDisposable);
185       }
186       session.addSessionListener(myDebugSessionListener, InstancesWindow.this.myDisposable);
187       final JavaDebuggerEditorsProvider editorsProvider = new JavaDebuggerEditorsProvider();
188
189       myFilterConditionEditor = new ExpressionEditorWithHistory(myProject, myClassName,
190                                                                 editorsProvider, InstancesWindow.this.myDisposable);
191
192       final Dimension filteringButtonSize = myFilterConditionEditor.getEditorComponent().getPreferredSize();
193       filteringButtonSize.width = JBUI.scale(FILTERING_BUTTON_ADDITIONAL_WIDTH) +
194                                   myFilterButton.getPreferredSize().width;
195       myFilterButton.setPreferredSize(filteringButtonSize);
196
197       final JBPanel filteringPane = new JBPanel(new BorderLayout(JBUI.scale(BORDER_LAYOUT_DEFAULT_GAP), 0));
198       final JBLabel sideEffectsWarning = new JBLabel("Warning: filtering may have side effects", SwingConstants.RIGHT);
199       sideEffectsWarning.setBorder(JBUI.Borders.emptyTop(1));
200       sideEffectsWarning.setComponentStyle(UIUtil.ComponentStyle.SMALL);
201       sideEffectsWarning.setFontColor(UIUtil.FontColor.BRIGHTER);
202
203       filteringPane.add(new JBLabel("Condition:"), BorderLayout.WEST);
204       filteringPane.add(myFilterConditionEditor.getComponent(), BorderLayout.CENTER);
205       filteringPane.add(myFilterButton, BorderLayout.EAST);
206       filteringPane.add(sideEffectsWarning, BorderLayout.SOUTH);
207
208       myProgress.addStopActionListener(this::cancelFilteringTask);
209
210       myInstancesTree = new InstancesTree(myProject, editorsProvider, markers, this::updateInstances);
211
212       myFilterButton.addActionListener(e -> {
213         final String expression = myFilterConditionEditor.getExpression().getExpression();
214         if (!expression.isEmpty()) {
215           myFilterConditionEditor.saveTextInHistory();
216         }
217
218         myFilterButton.setEnabled(false);
219         myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES);
220       });
221
222
223       final StackFrameList list = new StackFrameList(myDebugProcess);
224
225       list.addListSelectionListener(e -> list.navigateToSelectedValue(false));
226       new DoubleClickListener() {
227         @Override
228         protected boolean onDoubleClick(MouseEvent event) {
229           list.navigateToSelectedValue(true);
230           return true;
231         }
232       }.installOn(list);
233
234       final InstancesWithStackFrameView instancesWithStackFrame = new InstancesWithStackFrameView(session,
235                                                                                                   myInstancesTree, list, myClassName);
236
237       add(filteringPane, BorderLayout.NORTH);
238       add(instancesWithStackFrame.getComponent(), BorderLayout.CENTER);
239
240       final JComponent focusedComponent = myFilterConditionEditor.getEditorComponent();
241       UiNotifyConnector.doWhenFirstShown(focusedComponent, () ->
242         IdeFocusManager.findInstanceByComponent(focusedComponent)
243           .requestFocus(focusedComponent, true));
244     }
245
246     @Override
247     public void dispose() {
248       cancelFilteringTask();
249       Disposer.dispose(myInstancesTree);
250     }
251
252     private void updateInstances() {
253       cancelFilteringTask();
254       final XExpression filteringExpression = myFilterConditionEditor.getExpression();
255
256       myDebugProcess.getManagerThread().schedule(new DebuggerContextCommandImpl(myDebugProcess.getDebuggerContext()) {
257         @Override
258         public Priority getPriority() {
259           return Priority.LOWEST;
260         }
261
262         @Override
263         public void threadAction(@NotNull SuspendContextImpl suspendContext) {
264           myIsAndroidVM = DebuggerUtils.isAndroidVM(myDebugProcess.getVirtualMachineProxy().getVirtualMachine());
265           final int limit = myIsAndroidVM
266                             ? AndroidUtil.ANDROID_INSTANCES_LIMIT
267                             : DEFAULT_INSTANCES_LIMIT;
268           List<ObjectReference> instances = myInstancesProvider.getInstances(limit + 1);
269
270           final EvaluationContextImpl evaluationContext = myDebugProcess
271             .getDebuggerContext().createEvaluationContext();
272
273           if (instances.size() > limit) {
274             addWarningMessage(String.format("Not all instances will be loaded (only %d)", limit));
275             instances = instances.subList(0, limit);
276           }
277
278           if (evaluationContext != null) {
279             synchronized (myFilteringTaskLock) {
280               myFilteringTask = new MyFilteringWorker(instances, filteringExpression, evaluationContext);
281               myFilteringTask.execute();
282             }
283           }
284         }
285       });
286     }
287
288     private void cancelFilteringTask() {
289       if (myFilteringTask != null) {
290         synchronized (myFilteringTaskLock) {
291           if (myFilteringTask != null) {
292             myFilteringTask.cancel();
293             myFilteringTask = null;
294           }
295         }
296       }
297     }
298
299     private XValueMarkers<?, ?> getValueMarkers(@NotNull XDebugSession session) {
300       return session instanceof XDebugSessionImpl
301              ? ((XDebugSessionImpl)session).getValueMarkers()
302              : null;
303     }
304
305     private class MySessionListener implements XDebugSessionListener {
306       private volatile XDebuggerTreeState myTreeState = null;
307
308       @Override
309       public void sessionResumed() {
310         ApplicationManager.getApplication().invokeLater(() -> {
311           myTreeState = XDebuggerTreeState.saveState(myInstancesTree);
312           cancelFilteringTask();
313
314           myInstancesTree.setInfoMessage(
315             "The application is running");
316         });
317       }
318
319       @Override
320       public void sessionPaused() {
321         ApplicationManager.getApplication().invokeLater(() -> {
322           myProgress.setVisible(true);
323           final XDebuggerTreeState state = myTreeState;
324           if (state != null) {
325             myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES, state);
326           }
327           else {
328             myInstancesTree.rebuildTree(InstancesTree.RebuildPolicy.RELOAD_INSTANCES);
329           }
330         });
331       }
332     }
333
334     private class MyActionListener extends AnActionListener.Adapter {
335       private final XValueMarkers<?, ?> myValueMarkers;
336
337       private MyActionListener(@NotNull XValueMarkers<?, ?> markers) {
338         myValueMarkers = markers;
339       }
340
341       @Override
342       public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
343         if (dataContext.getData(PlatformDataKeys.CONTEXT_COMPONENT) == myInstancesTree &&
344             (isAddToWatchesAction(action) || isEvaluateExpressionAction(action))) {
345           XValueNodeImpl selectedNode = XDebuggerTreeActionBase.getSelectedNode(dataContext);
346
347           if (selectedNode != null) {
348             TreeNode currentNode = selectedNode;
349             while (!myInstancesTree.getRoot().equals(currentNode.getParent())) {
350               currentNode = currentNode.getParent();
351             }
352
353             final XValue valueContainer = ((XValueNodeImpl)currentNode).getValueContainer();
354
355             final String expression = valueContainer.getEvaluationExpression();
356             if (expression != null) {
357               myValueMarkers.markValue(valueContainer,
358                                        new ValueMarkup(expression.replace("@", ""), new JBColor(0, 0), null));
359             }
360
361             ApplicationManager.getApplication().invokeLater(() -> myInstancesTree
362               .rebuildTree(InstancesTree.RebuildPolicy.ONLY_UPDATE_LABELS));
363           }
364         }
365       }
366
367       private boolean isAddToWatchesAction(AnAction action) {
368         final String className = action.getClass().getSimpleName();
369         return action instanceof XDebuggerTreeActionBase && className.equals("XAddToWatchesAction");
370       }
371
372       private boolean isEvaluateExpressionAction(AnAction action) {
373         final String className = action.getClass().getSimpleName();
374         return action instanceof XDebuggerActionBase && className.equals("EvaluateAction");
375       }
376     }
377
378     private class MyFilteringCallback implements FilteringTaskCallback {
379       private final ErrorsValueGroup myErrorsGroup = new ErrorsValueGroup();
380       private final EvaluationContextImpl myEvaluationContext;
381
382       private long myFilteringStartedTime;
383
384       private int myProceedCount = 0;
385       private int myMatchedCount = 0;
386       private int myErrorsCount = 0;
387
388       private long myLastTreeUpdatingTime;
389       private long myLastProgressUpdatingTime;
390
391       public MyFilteringCallback(@NotNull EvaluationContextImpl evaluationContext) {
392         myEvaluationContext = evaluationContext;
393       }
394
395       private XValueChildrenList myChildren = new XValueChildrenList();
396
397       @Override
398       public void started(int total) {
399         myFilteringStartedTime = System.nanoTime();
400         myLastTreeUpdatingTime = myFilteringStartedTime;
401         myLastProgressUpdatingTime = System.nanoTime();
402         ApplicationManager.getApplication().invokeLater(() -> myProgress.start(total));
403       }
404
405       @NotNull
406       @Override
407       public Action matched(@NotNull Value ref) {
408         final JavaValue val = new InstanceJavaValue(new InstanceValueDescriptor(myProject, ref),
409                                                     myEvaluationContext, myNodeManager);
410         myMatchedCount++;
411         myProceedCount++;
412         myChildren.add(val);
413         updateProgress();
414         updateTree();
415
416         return myMatchedCount < MAX_TREE_NODE_COUNT ? Action.CONTINUE : Action.STOP;
417       }
418
419       @NotNull
420       @Override
421       public Action notMatched(@NotNull Value ref) {
422         myProceedCount++;
423         updateProgress();
424
425         return Action.CONTINUE;
426       }
427
428       @NotNull
429       @Override
430       public Action error(@NotNull Value ref, @NotNull String description) {
431         final JavaValue val = new InstanceJavaValue(new InstanceValueDescriptor(myProject, ref),
432                                                     myEvaluationContext, myNodeManager);
433         myErrorsGroup.addErrorValue(description, val);
434         myProceedCount++;
435         myErrorsCount++;
436         updateProgress();
437         return Action.CONTINUE;
438       }
439
440       @Override
441       public void completed(@NotNull FilteringResult reason) {
442         if (!myErrorsGroup.isEmpty()) {
443           myChildren.addBottomGroup(myErrorsGroup);
444         }
445
446         final long duration = System.nanoTime() - myFilteringStartedTime;
447         LOG.info(String.format("Filtering completed in %d ms for %d instances",
448                                TimeUnit.NANOSECONDS.toMillis(duration),
449                                myProceedCount));
450
451         final int proceed = myProceedCount;
452         final int matched = myMatchedCount;
453         final int errors = myErrorsCount;
454         final XValueChildrenList childrenList = myChildren;
455         ApplicationManager.getApplication().invokeLater(() -> {
456           myProgress.updateProgress(proceed, matched, errors);
457           myInstancesTree.addChildren(childrenList, true);
458           myFilterButton.setEnabled(true);
459           myProgress.complete(reason);
460         });
461       }
462
463       private void updateProgress() {
464         final long now = System.nanoTime();
465         if (now - myLastProgressUpdatingTime > TimeUnit.MILLISECONDS.toNanos(FILTERING_PROGRESS_UPDATING_MIN_DELAY_MILLIS)) {
466           final int proceed = myProceedCount;
467           final int matched = myMatchedCount;
468           final int errors = myErrorsCount;
469           ApplicationManager.getApplication().invokeLater(() -> myProgress.updateProgress(proceed, matched, errors));
470           myLastProgressUpdatingTime = now;
471         }
472       }
473
474       private void updateTree() {
475         final long now = System.nanoTime();
476         final int newChildrenCount = myChildren.size();
477         if (newChildrenCount >= FILTERING_CHUNK_SIZE ||
478             (newChildrenCount > 0 && now - myLastTreeUpdatingTime > TimeUnit.SECONDS.toNanos(MAX_DURATION_TO_UPDATE_TREE_SECONDS))) {
479           final XValueChildrenList children = myChildren;
480           ApplicationManager.getApplication().invokeLater(() -> myInstancesTree.addChildren(children, false));
481           myChildren = new XValueChildrenList();
482           myLastTreeUpdatingTime = System.nanoTime();
483         }
484       }
485     }
486
487     private class MyFilteringWorker extends SwingWorker<Void, Void> {
488       private final FilteringTask myTask;
489
490       MyFilteringWorker(@NotNull List<ObjectReference> refs,
491                         @NotNull XExpression expression,
492                         @NotNull EvaluationContextImpl evaluationContext) {
493         myTask = new FilteringTask(myClassName, myDebugProcess, expression, new MyValuesList(refs),
494                                    new MyFilteringCallback(evaluationContext));
495       }
496
497       @Override
498       protected Void doInBackground() throws Exception {
499         try {
500           myTask.run();
501         } catch (Throwable e) {
502           LOG.error(e);
503         }
504         return null;
505       }
506
507       public void cancel() {
508         myTask.cancel();
509         super.cancel(false);
510       }
511     }
512   }
513
514   private static class MyValuesList implements FilteringTask.ValuesList {
515     private final List<ObjectReference> myRefs;
516
517     public MyValuesList(List<ObjectReference> refs) {
518       myRefs = refs;
519     }
520
521     @Override
522     public int size() {
523       return myRefs.size();
524     }
525
526     @Override
527     public ObjectReference get(int index) {
528       return myRefs.get(index);
529     }
530   }
531
532   private final static class MyNodeManager extends NodeManagerImpl {
533     MyNodeManager(Project project) {
534       super(project, null);
535     }
536
537     @NotNull
538     @Override
539     public DebuggerTreeNodeImpl createNode(final NodeDescriptor descriptor, EvaluationContext evaluationContext) {
540       return new DebuggerTreeNodeImpl(null, descriptor);
541     }
542
543     @Override
544     public DebuggerTreeNodeImpl createMessageNode(MessageDescriptor descriptor) {
545       return new DebuggerTreeNodeImpl(null, descriptor);
546     }
547
548     @NotNull
549     @Override
550     public DebuggerTreeNodeImpl createMessageNode(String message) {
551       return new DebuggerTreeNodeImpl(null, new MessageDescriptor(message));
552     }
553   }
554 }