1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.debugger.memory.ui;
4 import com.intellij.debugger.DebuggerManager;
5 import com.intellij.debugger.engine.*;
6 import com.intellij.debugger.engine.events.DebuggerCommandImpl;
7 import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
8 import com.intellij.debugger.memory.component.InstancesTracker;
9 import com.intellij.debugger.memory.component.MemoryViewDebugProcessData;
10 import com.intellij.debugger.memory.component.MemoryViewManager;
11 import com.intellij.debugger.memory.component.MemoryViewManagerState;
12 import com.intellij.debugger.memory.event.InstancesTrackerListener;
13 import com.intellij.debugger.memory.event.MemoryViewManagerListener;
14 import com.intellij.debugger.memory.tracking.ConstructorInstancesTracker;
15 import com.intellij.debugger.memory.tracking.TrackerForNewInstances;
16 import com.intellij.debugger.memory.tracking.TrackingType;
17 import com.intellij.debugger.memory.utils.AndroidUtil;
18 import com.intellij.debugger.memory.utils.KeyboardUtils;
19 import com.intellij.debugger.memory.utils.LowestPriorityCommand;
20 import com.intellij.debugger.memory.utils.SingleAlarmWithMutableDelay;
21 import com.intellij.debugger.requests.ClassPrepareRequestor;
22 import com.intellij.icons.AllIcons;
23 import com.intellij.notification.NotificationType;
24 import com.intellij.openapi.Disposable;
25 import com.intellij.openapi.actionSystem.*;
26 import com.intellij.openapi.actionSystem.impl.ActionButton;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.util.Disposer;
31 import com.intellij.openapi.wm.IdeFocusManager;
32 import com.intellij.ui.*;
33 import com.intellij.util.ui.JBDimension;
34 import com.intellij.util.ui.components.BorderLayoutPanel;
35 import com.intellij.xdebugger.XDebugSession;
36 import com.intellij.xdebugger.XDebugSessionListener;
37 import com.intellij.xdebugger.XDebuggerManager;
38 import com.intellij.xdebugger.impl.XDebuggerManagerImpl;
39 import com.sun.jdi.ObjectReference;
40 import com.sun.jdi.ReferenceType;
41 import com.sun.jdi.VirtualMachine;
42 import com.sun.jdi.request.ClassPrepareRequest;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
47 import javax.swing.event.DocumentEvent;
49 import java.awt.event.*;
50 import java.util.Collections;
51 import java.util.LinkedHashMap;
52 import java.util.List;
54 import java.util.concurrent.ConcurrentHashMap;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 import java.util.concurrent.atomic.AtomicInteger;
59 import static com.intellij.debugger.memory.ui.ClassesTable.DiffViewTableModel.CLASSNAME_COLUMN_INDEX;
60 import static com.intellij.debugger.memory.ui.ClassesTable.DiffViewTableModel.DIFF_COLUMN_INDEX;
62 public class ClassesFilteredView extends BorderLayoutPanel implements Disposable {
63 private static final Logger LOG = Logger.getInstance(ClassesFilteredView.class);
64 private static final double DELAY_BEFORE_INSTANCES_QUERY_COEFFICIENT = 0.5;
65 private static final double MAX_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(2);
66 private static final int DEFAULT_BATCH_SIZE = Integer.MAX_VALUE;
67 private static final String EMPTY_TABLE_CONTENT_WHEN_RUNNING = "The application is running";
68 private static final String EMPTY_TABLE_CONTENT_WHEN_STOPPED = "Classes are not available";
69 private static final String CLICKABLE_TABLE_CONTENT = "Click to load the classes list";
71 private final Project myProject;
72 private final SingleAlarmWithMutableDelay mySingleAlarm;
74 private final SearchTextField myFilterTextField = new FilterTextField();
75 private final ClassesTable myTable;
76 private final InstancesTracker myInstancesTracker;
77 private final Map<ReferenceType, ConstructorInstancesTracker> myConstructorTrackedClasses = new ConcurrentHashMap<>();
78 private final MyDebuggerSessionListener myDebugSessionListener;
80 // tick on each session paused event
81 private final AtomicInteger myTime = new AtomicInteger(0);
83 private final AtomicInteger myLastUpdatingTime = new AtomicInteger(Integer.MIN_VALUE);
86 * Indicates that the debug session had been stopped at least once.
88 * State: false to true
90 private final AtomicBoolean myIsTrackersActivated = new AtomicBoolean(false);
93 * Indicates that view is visible
95 private volatile boolean myIsActive;
97 public ClassesFilteredView(@NotNull XDebugSession debugSession,
98 @NotNull DebugProcessImpl debugProcess,
99 @NotNull InstancesTracker tracker) {
100 myProject = debugSession.getProject();
102 final DebuggerManagerThreadImpl managerThread = debugProcess.getManagerThread();
103 myInstancesTracker = tracker;
104 final InstancesTrackerListener instancesTrackerListener = new InstancesTrackerListener() {
106 public void classChanged(@NotNull String name, @NotNull TrackingType type) {
107 ReferenceType ref = myTable.getClassByName(name);
109 final boolean activated = myIsTrackersActivated.get();
110 managerThread.schedule(new DebuggerCommandImpl() {
112 protected void action() {
113 trackClass(debugSession, ref, type, activated);
121 public void classRemoved(@NotNull String name) {
122 ReferenceType ref = myTable.getClassByName(name);
123 if (ref != null && myConstructorTrackedClasses.containsKey(ref)) {
124 ConstructorInstancesTracker removed = myConstructorTrackedClasses.remove(ref);
125 Disposer.dispose(removed);
126 myTable.getRowSorter().allRowsChanged();
131 debugSession.addSessionListener(new XDebugSessionListener() {
133 public void sessionStopped() {
134 debugSession.removeSessionListener(this);
135 myInstancesTracker.removeTrackerListener(instancesTrackerListener);
139 debugProcess.addDebugProcessListener(new DebugProcessListener() {
141 public void processAttached(DebugProcess process) {
142 debugProcess.removeDebugProcessListener(this);
143 managerThread.invoke(new DebuggerCommandImpl() {
145 protected void action() {
146 final boolean activated = myIsTrackersActivated.get();
147 final VirtualMachineProxyImpl proxy = debugProcess.getVirtualMachineProxy();
148 tracker.getTrackedClasses().forEach((className, type) -> {
149 List<ReferenceType> classes = proxy.classesByName(className);
150 if (classes.isEmpty()) {
151 trackWhenPrepared(className, debugSession, debugProcess, type);
154 for (ReferenceType ref : classes) {
155 trackClass(debugSession, ref, type, activated);
160 tracker.addTrackerListener(instancesTrackerListener);
165 private void trackWhenPrepared(@NotNull String className,
166 @NotNull XDebugSession session,
167 @NotNull DebugProcessImpl process,
168 @NotNull TrackingType type) {
169 final ClassPrepareRequestor request = new ClassPrepareRequestor() {
171 public void processClassPrepare(DebugProcess debuggerProcess, ReferenceType referenceType) {
172 process.getRequestsManager().deleteRequest(this);
173 trackClass(session, referenceType, type, myIsTrackersActivated.get());
177 final ClassPrepareRequest classPrepareRequest = process.getRequestsManager()
178 .createClassPrepareRequest(request, className);
179 if (classPrepareRequest != null) {
180 classPrepareRequest.enable();
183 LOG.warn("Cannot create a 'class prepare' request. Class " + className + " not tracked.");
188 final MemoryViewManagerState memoryViewManagerState = MemoryViewManager.getInstance().getState();
190 myTable = new ClassesTable(tracker, this, memoryViewManagerState.isShowWithDiffOnly,
191 memoryViewManagerState.isShowWithInstancesOnly, memoryViewManagerState.isShowTrackedOnly);
192 myTable.getEmptyText().setText(EMPTY_TABLE_CONTENT_WHEN_RUNNING);
193 Disposer.register(this, myTable);
195 myTable.addMouseMotionListener(new MyMouseMotionListener());
196 myTable.addMouseListener(new MyOpenNewInstancesListener());
197 new MyDoubleClickListener().installOn(myTable);
199 myTable.addKeyListener(new KeyAdapter() {
201 public void keyReleased(KeyEvent e) {
202 final int keyCode = e.getKeyCode();
203 if (KeyboardUtils.isEnterKey(keyCode)) {
204 handleClassSelection(myTable.getSelectedClass());
206 else if (KeyboardUtils.isCharacter(keyCode) || KeyboardUtils.isBackSpace(keyCode)) {
207 final String text = myFilterTextField.getText();
208 final String newText = KeyboardUtils.isBackSpace(keyCode)
209 ? text.substring(0, text.length() - 1)
210 : text + e.getKeyChar();
211 myFilterTextField.setText(newText);
212 IdeFocusManager.getInstance(myProject).requestFocus(myFilterTextField, false);
217 myFilterTextField.addKeyboardListener(new KeyAdapter() {
219 public void keyPressed(KeyEvent e) {
224 public void keyReleased(KeyEvent e) {
228 private void dispatch(KeyEvent e) {
229 final int keyCode = e.getKeyCode();
230 if (myTable.isInClickableMode() && (KeyboardUtils.isCharacter(keyCode) || KeyboardUtils.isEnterKey(keyCode))) {
231 myTable.exitClickableMode();
232 updateClassesAndCounts(true);
234 else if (KeyboardUtils.isUpDownKey(keyCode) || KeyboardUtils.isEnterKey(keyCode)) {
235 myTable.dispatchEvent(e);
240 myFilterTextField.addDocumentListener(new DocumentAdapter() {
242 protected void textChanged(DocumentEvent e) {
243 myTable.setFilterPattern(myFilterTextField.getText());
247 final MemoryViewManagerListener memoryViewManagerListener = state -> {
248 myTable.setFilteringByDiffNonZero(state.isShowWithDiffOnly);
249 myTable.setFilteringByInstanceExists(state.isShowWithInstancesOnly);
250 myTable.setFilteringByTrackingState(state.isShowTrackedOnly);
251 if (state.isAutoUpdateModeOn && myTable.isInClickableMode()) {
252 updateClassesAndCounts(true);
256 MemoryViewManager.getInstance().addMemoryViewManagerListener(memoryViewManagerListener, this);
258 myDebugSessionListener = new MyDebuggerSessionListener();
259 debugSession.addSessionListener(myDebugSessionListener, this);
261 mySingleAlarm = new SingleAlarmWithMutableDelay(suspendContext -> {
262 ApplicationManager.getApplication().invokeLater(() -> myTable.setBusy(true));
263 suspendContext.getDebugProcess().getManagerThread().schedule(new MyUpdateClassesCommand(suspendContext));
266 mySingleAlarm.setDelay((int)TimeUnit.MILLISECONDS.toMillis(500));
268 myTable.addMouseListener(new PopupHandler() {
270 public void invokePopup(Component comp, int x, int y) {
271 ActionPopupMenu menu = createContextMenu();
272 menu.getComponent().show(comp, x, y);
276 final JScrollPane scroll = ScrollPaneFactory.createScrollPane(myTable, SideBorder.TOP);
277 final DefaultActionGroup group = (DefaultActionGroup)ActionManager.getInstance().getAction("MemoryView.SettingsPopupActionGroup");
278 group.setPopup(true);
279 final Presentation actionsPresentation = new Presentation("Memory View Settings");
280 actionsPresentation.setIcon(AllIcons.General.SecondaryGroup);
282 final ActionButton button = new ActionButton(group, actionsPresentation, ActionPlaces.UNKNOWN, new JBDimension(25, 25));
283 final BorderLayoutPanel topPanel = new BorderLayoutPanel();
284 topPanel.addToCenter(myFilterTextField);
285 topPanel.addToRight(button);
291 TrackerForNewInstances getStrategy(@NotNull ReferenceType ref) {
292 return myConstructorTrackedClasses.getOrDefault(ref, null);
295 private void trackClass(@NotNull XDebugSession session,
296 @NotNull ReferenceType ref,
297 @NotNull TrackingType type,
298 boolean isTrackerEnabled) {
299 LOG.assertTrue(DebuggerManager.getInstance(myProject).isDebuggerManagerThread());
300 if (type == TrackingType.CREATION) {
301 final ConstructorInstancesTracker old = myConstructorTrackedClasses.getOrDefault(ref, null);
303 Disposer.dispose(old);
306 final ConstructorInstancesTracker tracker = new ConstructorInstancesTracker(ref, session, myInstancesTracker);
307 tracker.setBackgroundMode(!myIsActive);
308 if (isTrackerEnabled) {
315 myConstructorTrackedClasses.put(ref, tracker);
319 private void handleClassSelection(@Nullable ReferenceType ref) {
320 final XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
321 if (ref != null && debugSession != null && debugSession.isSuspended()) {
322 if (!ref.virtualMachine().canGetInstanceInfo()) {
323 XDebuggerManagerImpl.NOTIFICATION_GROUP
324 .createNotification("The virtual machine implementation does not provide an ability to get instances",
325 NotificationType.INFORMATION).notify(debugSession.getProject());
329 new InstancesWindow(debugSession, limit -> {
330 final List<ObjectReference> instances = ref.instances(limit);
331 return instances == null ? Collections.emptyList() : instances;
332 }, ref.name()).show();
336 private void commitAllTrackers() {
337 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::commitTracked);
340 private void updateClassesAndCounts(boolean immediate) {
341 ApplicationManager.getApplication().invokeLater(() -> {
342 final XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
343 if (debugSession != null) {
344 final DebugProcess debugProcess = DebuggerManager.getInstance(myProject)
345 .getDebugProcess(debugSession.getDebugProcess().getProcessHandler());
346 if (debugProcess != null && debugProcess.isAttached() && debugProcess instanceof DebugProcessImpl) {
347 final DebugProcessImpl process = (DebugProcessImpl)debugProcess;
348 final SuspendContextImpl context = process.getDebuggerContext().getSuspendContext();
349 if (context != null) {
351 mySingleAlarm.cancelAndRequestImmediate(context);
354 mySingleAlarm.cancelAndRequest(context);
359 }, myProject.getDisposed());
362 private static ActionPopupMenu createContextMenu() {
363 final ActionGroup group = (ActionGroup)ActionManager.getInstance().getAction("MemoryView.ClassesPopupActionGroup");
364 return ActionManager.getInstance().createActionPopupMenu("MemoryView.ClassesPopupActionGroup", group);
368 public void dispose() {
369 myConstructorTrackedClasses.clear();
372 public void setActive(boolean active, @NotNull DebuggerManagerThreadImpl managerThread) {
373 if (myIsActive == active) {
379 managerThread.schedule(new DebuggerCommandImpl() {
381 protected void action() {
392 private void doActivate() {
393 myDebugSessionListener.setActive(true);
394 myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(false));
396 if (isNeedUpdateView()) {
397 if (MemoryViewManager.getInstance().isAutoUpdateModeEnabled()) {
398 updateClassesAndCounts(true);
401 makeTableClickable();
406 private void makeTableClickable() {
407 ApplicationManager.getApplication().invokeLater(
408 () -> myTable.makeClickable(CLICKABLE_TABLE_CONTENT, () -> updateClassesAndCounts(true)));
411 private void doPause() {
412 myDebugSessionListener.setActive(false);
413 mySingleAlarm.cancelAllRequests();
414 myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(true));
417 private boolean isNeedUpdateView() {
418 return myLastUpdatingTime.get() != myTime.get();
421 private void viewUpdated() {
422 myLastUpdatingTime.set(myTime.get());
425 private final class MyUpdateClassesCommand extends LowestPriorityCommand {
427 MyUpdateClassesCommand(@Nullable SuspendContextImpl suspendContext) {
428 super(suspendContext);
432 public void contextAction(@NotNull SuspendContextImpl suspendContext) {
435 final VirtualMachineProxyImpl proxy = suspendContext.getDebugProcess().getVirtualMachineProxy();
436 final List<ReferenceType> classes = proxy.allClasses();
438 if (!classes.isEmpty()) {
439 final VirtualMachine vm = classes.get(0).virtualMachine();
440 if (proxy.canGetInstanceInfo()) {
441 final Map<ReferenceType, Long> counts = getInstancesCounts(classes, vm);
442 ApplicationManager.getApplication().invokeLater(() -> myTable.updateContent(counts));
445 ApplicationManager.getApplication().invokeLater(() -> myTable.updateClassesOnly(classes));
449 ApplicationManager.getApplication().invokeLater(() -> myTable.setBusy(false));
453 private void handleTrackers() {
454 if (!myIsTrackersActivated.get()) {
455 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::enable);
456 myIsTrackersActivated.set(true);
463 private Map<ReferenceType, Long> getInstancesCounts(@NotNull List<ReferenceType> classes, @NotNull VirtualMachine vm) {
464 final int batchSize = AndroidUtil.isAndroidVM(vm)
465 ? AndroidUtil.ANDROID_COUNT_BY_CLASSES_BATCH_SIZE
466 : DEFAULT_BATCH_SIZE;
468 final int size = classes.size();
469 final Map<ReferenceType, Long> result = new LinkedHashMap<>();
471 for (int begin = 0, end = Math.min(batchSize, size);
473 begin = end, end = Math.min(end + batchSize, size)) {
474 final List<ReferenceType> batch = classes.subList(begin, end);
476 final long start = System.nanoTime();
477 final long[] counts = vm.instanceCounts(batch);
478 final long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
480 for (int i = 0; i < batch.size(); i++) {
481 result.put(batch.get(i), counts[i]);
484 final int waitTime = (int)Math.min(DELAY_BEFORE_INSTANCES_QUERY_COEFFICIENT * delay, MAX_DELAY_MILLIS);
485 mySingleAlarm.setDelay(waitTime);
486 LOG.debug(String.format("Instances query time = %d ms. Count of classes = %d", delay, batch.size()));
493 private static class FilterTextField extends SearchTextField {
499 protected void showPopup() {
503 protected boolean hasIconsOutsideOfTextField() {
508 private class MyOpenNewInstancesListener extends MouseAdapter {
510 public void mouseClicked(MouseEvent e) {
511 if (e.getClickCount() != 1 || e.getButton() != MouseEvent.BUTTON1 || !isShowNewInstancesEvent(e)) {
515 final ReferenceType ref = myTable.getSelectedClass();
516 final TrackerForNewInstances strategy = ref == null ? null : getStrategy(ref);
517 XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
518 if (strategy != null && debugSession != null) {
519 final DebugProcess debugProcess =
520 DebuggerManager.getInstance(myProject).getDebugProcess(debugSession.getDebugProcess().getProcessHandler());
521 final MemoryViewDebugProcessData data = debugProcess.getUserData(MemoryViewDebugProcessData.KEY);
523 final List<ObjectReference> newInstances = strategy.getNewInstances();
524 data.getTrackedStacks().pinStacks(ref);
525 final InstancesWindow instancesWindow = new InstancesWindow(debugSession, limit -> newInstances, ref.name());
526 Disposer.register(instancesWindow.getDisposable(), () -> data.getTrackedStacks().unpinStacks(ref));
527 instancesWindow.show();
530 LOG.warn("MemoryViewDebugProcessData not found in debug session user data");
536 private class MyDoubleClickListener extends DoubleClickListener {
538 protected boolean onDoubleClick(MouseEvent event) {
539 if (!isShowNewInstancesEvent(event)) {
540 handleClassSelection(myTable.getSelectedClass());
548 private class MyMouseMotionListener implements MouseMotionListener {
550 public void mouseDragged(MouseEvent e) {
554 public void mouseMoved(MouseEvent e) {
555 if (myTable.isInClickableMode()) return;
557 if (isShowNewInstancesEvent(e)) {
558 myTable.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
561 myTable.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
566 private boolean isShowNewInstancesEvent(@NotNull MouseEvent e) {
567 final int col = myTable.columnAtPoint(e.getPoint());
568 final int row = myTable.rowAtPoint(e.getPoint());
569 if (col == -1 || row == -1 || myTable.convertColumnIndexToModel(col) != DIFF_COLUMN_INDEX) {
573 final int modelRow = myTable.convertRowIndexToModel(row);
575 final ReferenceType ref = (ReferenceType)myTable.getModel().getValueAt(modelRow, CLASSNAME_COLUMN_INDEX);
576 final ConstructorInstancesTracker tracker = myConstructorTrackedClasses.getOrDefault(ref, null);
578 return tracker != null && tracker.isReady() && tracker.getCount() > 0;
581 private class MyDebuggerSessionListener implements XDebugSessionListener {
582 private volatile boolean myIsActive = false;
584 void setActive(boolean value) {
589 public void sessionResumed() {
591 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::obsolete);
592 ApplicationManager.getApplication().invokeLater(() -> myTable.hideContent(EMPTY_TABLE_CONTENT_WHEN_RUNNING));
594 mySingleAlarm.cancelAllRequests();
599 public void sessionStopped() {
600 myConstructorTrackedClasses.values().forEach(Disposer::dispose);
601 myConstructorTrackedClasses.clear();
602 mySingleAlarm.cancelAllRequests();
603 ApplicationManager.getApplication().invokeLater(() -> myTable.clean(EMPTY_TABLE_CONTENT_WHEN_STOPPED));
607 public void sessionPaused() {
608 myTime.incrementAndGet();
610 if (MemoryViewManager.getInstance().isAutoUpdateModeEnabled()) {
611 updateClassesAndCounts(false);
614 makeTableClickable();