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 if (KeyboardUtils.isUpDownKey(e.getKeyCode()) || KeyboardUtils.isEnterKey(e.getKeyCode())) {
230 myTable.dispatchEvent(e);
235 myFilterTextField.addDocumentListener(new DocumentAdapter() {
237 protected void textChanged(DocumentEvent e) {
238 myTable.setFilterPattern(myFilterTextField.getText());
242 final MemoryViewManagerListener memoryViewManagerListener = state -> {
243 myTable.setFilteringByDiffNonZero(state.isShowWithDiffOnly);
244 myTable.setFilteringByInstanceExists(state.isShowWithInstancesOnly);
245 myTable.setFilteringByTrackingState(state.isShowTrackedOnly);
246 if (state.isAutoUpdateModeOn && myTable.isInClickableMode()) {
247 updateClassesAndCounts(true);
251 MemoryViewManager.getInstance().addMemoryViewManagerListener(memoryViewManagerListener, this);
253 myDebugSessionListener = new MyDebuggerSessionListener();
254 debugSession.addSessionListener(myDebugSessionListener, this);
256 mySingleAlarm = new SingleAlarmWithMutableDelay(suspendContext -> {
257 ApplicationManager.getApplication().invokeLater(() -> myTable.setBusy(true));
258 suspendContext.getDebugProcess().getManagerThread().schedule(new MyUpdateClassesCommand(suspendContext));
261 mySingleAlarm.setDelay((int)TimeUnit.MILLISECONDS.toMillis(500));
263 myTable.addMouseListener(new PopupHandler() {
265 public void invokePopup(Component comp, int x, int y) {
266 ActionPopupMenu menu = createContextMenu();
267 menu.getComponent().show(comp, x, y);
271 final JScrollPane scroll = ScrollPaneFactory.createScrollPane(myTable, SideBorder.TOP);
272 final DefaultActionGroup group = (DefaultActionGroup)ActionManager.getInstance().getAction("MemoryView.SettingsPopupActionGroup");
273 group.setPopup(true);
274 final Presentation actionsPresentation = new Presentation("Memory View Settings");
275 actionsPresentation.setIcon(AllIcons.General.SecondaryGroup);
277 final ActionButton button = new ActionButton(group, actionsPresentation, ActionPlaces.UNKNOWN, new JBDimension(25, 25));
278 final BorderLayoutPanel topPanel = new BorderLayoutPanel();
279 topPanel.addToCenter(myFilterTextField);
280 topPanel.addToRight(button);
286 TrackerForNewInstances getStrategy(@NotNull ReferenceType ref) {
287 return myConstructorTrackedClasses.getOrDefault(ref, null);
290 private void trackClass(@NotNull XDebugSession session,
291 @NotNull ReferenceType ref,
292 @NotNull TrackingType type,
293 boolean isTrackerEnabled) {
294 LOG.assertTrue(DebuggerManager.getInstance(myProject).isDebuggerManagerThread());
295 if (type == TrackingType.CREATION) {
296 final ConstructorInstancesTracker old = myConstructorTrackedClasses.getOrDefault(ref, null);
298 Disposer.dispose(old);
301 final ConstructorInstancesTracker tracker = new ConstructorInstancesTracker(ref, session, myInstancesTracker);
302 tracker.setBackgroundMode(!myIsActive);
303 if (isTrackerEnabled) {
310 myConstructorTrackedClasses.put(ref, tracker);
314 private void handleClassSelection(@Nullable ReferenceType ref) {
315 final XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
316 if (ref != null && debugSession != null && debugSession.isSuspended()) {
317 if (!ref.virtualMachine().canGetInstanceInfo()) {
318 XDebuggerManagerImpl.NOTIFICATION_GROUP
319 .createNotification("The virtual machine implementation does not provide an ability to get instances",
320 NotificationType.INFORMATION).notify(debugSession.getProject());
324 new InstancesWindow(debugSession, limit -> {
325 final List<ObjectReference> instances = ref.instances(limit);
326 return instances == null ? Collections.emptyList() : instances;
327 }, ref.name()).show();
331 private void commitAllTrackers() {
332 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::commitTracked);
335 private void updateClassesAndCounts(boolean immediate) {
336 ApplicationManager.getApplication().invokeLater(() -> {
337 final XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
338 if (debugSession != null) {
339 final DebugProcess debugProcess = DebuggerManager.getInstance(myProject)
340 .getDebugProcess(debugSession.getDebugProcess().getProcessHandler());
341 if (debugProcess != null && debugProcess.isAttached() && debugProcess instanceof DebugProcessImpl) {
342 final DebugProcessImpl process = (DebugProcessImpl)debugProcess;
343 final SuspendContextImpl context = process.getDebuggerContext().getSuspendContext();
344 if (context != null) {
346 mySingleAlarm.cancelAndRequestImmediate(context);
349 mySingleAlarm.cancelAndRequest(context);
354 }, myProject.getDisposed());
357 private static ActionPopupMenu createContextMenu() {
358 final ActionGroup group = (ActionGroup)ActionManager.getInstance().getAction("MemoryView.ClassesPopupActionGroup");
359 return ActionManager.getInstance().createActionPopupMenu("MemoryView.ClassesPopupActionGroup", group);
363 public void dispose() {
364 myConstructorTrackedClasses.clear();
367 public void setActive(boolean active, @NotNull DebuggerManagerThreadImpl managerThread) {
368 if (myIsActive == active) {
374 managerThread.schedule(new DebuggerCommandImpl() {
376 protected void action() {
387 private void doActivate() {
388 myDebugSessionListener.setActive(true);
389 myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(false));
391 if (isNeedUpdateView()) {
392 if (MemoryViewManager.getInstance().isAutoUpdateModeEnabled()) {
393 updateClassesAndCounts(true);
396 makeTableClickable();
401 private void makeTableClickable() {
402 ApplicationManager.getApplication().invokeLater(
403 () -> myTable.makeClickable(CLICKABLE_TABLE_CONTENT, () -> updateClassesAndCounts(true)));
406 private void doPause() {
407 myDebugSessionListener.setActive(false);
408 mySingleAlarm.cancelAllRequests();
409 myConstructorTrackedClasses.values().forEach(x -> x.setBackgroundMode(true));
412 private boolean isNeedUpdateView() {
413 return myLastUpdatingTime.get() != myTime.get();
416 private void viewUpdated() {
417 myLastUpdatingTime.set(myTime.get());
420 private final class MyUpdateClassesCommand extends LowestPriorityCommand {
422 MyUpdateClassesCommand(@Nullable SuspendContextImpl suspendContext) {
423 super(suspendContext);
427 public void contextAction(@NotNull SuspendContextImpl suspendContext) {
430 final List<ReferenceType> classes = suspendContext.getDebugProcess().getVirtualMachineProxy().allClasses();
432 if (!classes.isEmpty()) {
433 final VirtualMachine vm = classes.get(0).virtualMachine();
434 if (vm.canGetInstanceInfo()) {
435 final Map<ReferenceType, Long> counts = getInstancesCounts(classes, vm);
436 ApplicationManager.getApplication().invokeLater(() -> myTable.updateContent(counts));
439 ApplicationManager.getApplication().invokeLater(() -> myTable.updateClassesOnly(classes));
443 ApplicationManager.getApplication().invokeLater(() -> myTable.setBusy(false));
447 private void handleTrackers() {
448 if (!myIsTrackersActivated.get()) {
449 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::enable);
450 myIsTrackersActivated.set(true);
457 private Map<ReferenceType, Long> getInstancesCounts(@NotNull List<ReferenceType> classes, @NotNull VirtualMachine vm) {
458 final int batchSize = AndroidUtil.isAndroidVM(vm)
459 ? AndroidUtil.ANDROID_COUNT_BY_CLASSES_BATCH_SIZE
460 : DEFAULT_BATCH_SIZE;
462 final int size = classes.size();
463 final Map<ReferenceType, Long> result = new LinkedHashMap<>();
465 for (int begin = 0, end = Math.min(batchSize, size);
467 begin = end, end = Math.min(end + batchSize, size)) {
468 final List<ReferenceType> batch = classes.subList(begin, end);
470 final long start = System.nanoTime();
471 final long[] counts = vm.instanceCounts(batch);
472 final long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
474 for (int i = 0; i < batch.size(); i++) {
475 result.put(batch.get(i), counts[i]);
478 final int waitTime = (int)Math.min(DELAY_BEFORE_INSTANCES_QUERY_COEFFICIENT * delay, MAX_DELAY_MILLIS);
479 mySingleAlarm.setDelay(waitTime);
480 LOG.debug(String.format("Instances query time = %d ms. Count of classes = %d", delay, batch.size()));
487 private static class FilterTextField extends SearchTextField {
493 protected void showPopup() {
497 protected boolean hasIconsOutsideOfTextField() {
502 private class MyOpenNewInstancesListener extends MouseAdapter {
504 public void mouseClicked(MouseEvent e) {
505 if (e.getClickCount() != 1 || e.getButton() != MouseEvent.BUTTON1 || !isShowNewInstancesEvent(e)) {
509 final ReferenceType ref = myTable.getSelectedClass();
510 final TrackerForNewInstances strategy = ref == null ? null : getStrategy(ref);
511 XDebugSession debugSession = XDebuggerManager.getInstance(myProject).getCurrentSession();
512 if (strategy != null && debugSession != null) {
513 final DebugProcess debugProcess =
514 DebuggerManager.getInstance(myProject).getDebugProcess(debugSession.getDebugProcess().getProcessHandler());
515 final MemoryViewDebugProcessData data = debugProcess.getUserData(MemoryViewDebugProcessData.KEY);
517 final List<ObjectReference> newInstances = strategy.getNewInstances();
518 data.getTrackedStacks().pinStacks(ref);
519 final InstancesWindow instancesWindow = new InstancesWindow(debugSession, limit -> newInstances, ref.name());
520 Disposer.register(instancesWindow.getDisposable(), () -> data.getTrackedStacks().unpinStacks(ref));
521 instancesWindow.show();
524 LOG.warn("MemoryViewDebugProcessData not found in debug session user data");
530 private class MyDoubleClickListener extends DoubleClickListener {
532 protected boolean onDoubleClick(MouseEvent event) {
533 if (!isShowNewInstancesEvent(event)) {
534 handleClassSelection(myTable.getSelectedClass());
542 private class MyMouseMotionListener implements MouseMotionListener {
544 public void mouseDragged(MouseEvent e) {
548 public void mouseMoved(MouseEvent e) {
549 if (myTable.isInClickableMode()) return;
551 if (isShowNewInstancesEvent(e)) {
552 myTable.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
555 myTable.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
560 private boolean isShowNewInstancesEvent(@NotNull MouseEvent e) {
561 final int col = myTable.columnAtPoint(e.getPoint());
562 final int row = myTable.rowAtPoint(e.getPoint());
563 if (col == -1 || row == -1 || myTable.convertColumnIndexToModel(col) != DIFF_COLUMN_INDEX) {
567 final int modelRow = myTable.convertRowIndexToModel(row);
569 final ReferenceType ref = (ReferenceType)myTable.getModel().getValueAt(modelRow, CLASSNAME_COLUMN_INDEX);
570 final ConstructorInstancesTracker tracker = myConstructorTrackedClasses.getOrDefault(ref, null);
572 return tracker != null && tracker.isReady() && tracker.getCount() > 0;
575 private class MyDebuggerSessionListener implements XDebugSessionListener {
576 private volatile boolean myIsActive = false;
578 void setActive(boolean value) {
583 public void sessionResumed() {
585 myConstructorTrackedClasses.values().forEach(ConstructorInstancesTracker::obsolete);
586 ApplicationManager.getApplication().invokeLater(() -> myTable.hideContent(EMPTY_TABLE_CONTENT_WHEN_RUNNING));
588 mySingleAlarm.cancelAllRequests();
593 public void sessionStopped() {
594 myConstructorTrackedClasses.values().forEach(Disposer::dispose);
595 myConstructorTrackedClasses.clear();
596 mySingleAlarm.cancelAllRequests();
597 ApplicationManager.getApplication().invokeLater(() -> myTable.clean(EMPTY_TABLE_CONTENT_WHEN_STOPPED));
601 public void sessionPaused() {
602 myTime.incrementAndGet();
604 if (MemoryViewManager.getInstance().isAutoUpdateModeEnabled()) {
605 updateClassesAndCounts(false);
608 makeTableClickable();