[debugger] IJPL-249428 Avoid url calls in debugger frontend

(cherry picked from commit 9197ec1661bbc51c198eacc12a67d95542c53f2b)

GitOrigin-RevId: 13da914fd58a51d980a2f1a62b6b3ac5151db65d
This commit is contained in:
Maksim Zuev
2026-08-19 18:08:12 +00:00
committed by intellij-monorepo-bot
parent 32eae71985
commit a49b48d542
6 changed files with 73 additions and 57 deletions
@@ -416,7 +416,7 @@ class FrontendXBreakpointManager(private val project: Project, private val cs: C
override fun findBreakpointsAtLine(type: XLineBreakpointTypeProxy, file: VirtualFile, line: Int, placement: XLineBreakpointVerticalPlacement): List<XLineBreakpointProxy> {
return breakpoints.values.filterIsInstance<XLineBreakpointProxy>().filter {
it.type == type && it.getFile()?.url == file.url && it.getLine() == line &&
it.type == type && it.getFile() == file && it.getLine() == line &&
it.getPlacement() == placement
}
}
@@ -220,7 +220,7 @@ internal class HotSwapFloatingToolbarProvider : FloatingToolbarProvider {
override fun register(dataContext: DataContext, component: FloatingToolbarComponent, parentDisposable: Disposable) {
val project = dataContext.getData(CommonDataKeys.PROJECT) ?: return
val editorTag = dataContext.editorTag
val editorTag = if (logger.isDebugEnabled) dataContext.editorTag else ""
if (component is JComponent) {
component.installPopupMenu()
component.accessibleContext.accessibleName = HotSwapUiExtension.computeSafeIfAvailable { it.toolbarAccessibleName }
@@ -148,7 +148,7 @@ public final class XBreakpointManagerImpl implements XBreakpointManager {
messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkVirtualFileListenerAdapter(new VirtualFileUrlChangeAdapter() {
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
myLineBreakpointManager.onFileDeleted(event.getFile().getUrl());
myLineBreakpointManager.onFileDeleted(event.getFile());
}
@Override
@@ -19,6 +19,7 @@ import com.intellij.platform.debugger.impl.shared.XDebuggerWatchesManager;
import com.intellij.platform.debugger.impl.shared.proxy.XDebugManagerProxy;
import com.intellij.platform.debugger.impl.shared.proxy.XDebugSessionProxy;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.EDT;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
@@ -35,7 +36,6 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -43,7 +43,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ApiStatus.Internal
@@ -56,9 +55,9 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
*/
private final Map<String, List<XWatch>> watches = new ConcurrentHashMap<>();
/**
* Maps file URL to a set of inline watches.
* Maps file to its inline watches.
*/
private final Map<String, Set<InlineWatch>> inlineWatches = new ConcurrentHashMap<>();
private final MultiMap<VirtualFile, InlineWatch> inlineWatches = MultiMap.createConcurrent();
private final MergingUpdateQueue myInlinesUpdateQueue;
private final Project myProject;
@@ -117,7 +116,7 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
@Override
public @NotNull List<InlineWatch> getInlineWatches() {
return inlineWatches.values().stream().flatMap(l -> l.stream()).collect(Collectors.toList());
return List.copyOf(inlineWatches.values());
}
@ApiStatus.Internal
@@ -127,11 +126,9 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
watches.forEach((key, value) -> expressions.add(new ConfigurationState(key, value)));
List<InlineWatchState> inlineExpressionStates = state.getInlineExpressionStates();
inlineExpressionStates.clear();
inlineWatches.values().stream()
.flatMap(l -> l.stream())
.forEach((value) -> {
inlineExpressionStates.add(new InlineWatchState(value.getExpression(), value.getLine(), value.getPosition().getFile().getUrl()));
});
inlineWatches.values().forEach(value -> {
inlineExpressionStates.add(new InlineWatchState(value.getExpression(), value.getLine(), value.getPosition().getFile().getUrl()));
});
return state;
}
@@ -172,15 +169,15 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
if (position == null || expression == null) continue;
InlineWatch watch = new InlineWatch(expression, position);
inlineWatches.computeIfAbsent(inlineWatchState.getFileUrl(), (k) -> new HashSet<>()).add(watch);
inlineWatches.putValue(file, watch);
}
// set markers in the background
ApplicationManager.getApplication().executeOnPooledThread(() -> {
for (InlineWatch i : ContainerUtil.flatten(inlineWatches.values())) {
for (InlineWatch i : inlineWatches.values()) {
ReadAction.nonBlocking(() -> {
if (!i.setMarker()) {
inlineWatches.get(i.getPosition().getFile().getUrl()).remove(i);
inlineWatches.remove(i.getPosition().getFile(), i);
}
}).executeSynchronously();
}
@@ -198,7 +195,7 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
@Override
public void inlineWatchesRemoved(@NotNull List<InlineWatch> removed, XInlineWatchesView watchesView) {
inlineWatches.values().forEach(set -> removed.forEach(set::remove));
removeFromStorage(removed);
getWatchesViews().filter(v -> v != watchesView).forEach(view -> view.removeInlineWatches(removed));
}
@@ -218,8 +215,7 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
EDT.assertIsEdt();
InlineWatch watch = new InlineWatch(expression, position);
watch.setMarker();
String fileUrl = position.getFile().getUrl();
inlineWatches.computeIfAbsent(fileUrl, (k) -> new HashSet<>()).add(watch);
inlineWatches.putValue(position.getFile(), watch);
getWatchesViews().forEach(view -> view.addInlineWatchExpression(watch, index, navigateToWatchNode));
}
@@ -239,13 +235,16 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
}
private void removeInlines(Collection<InlineWatch> remove) {
for (InlineWatch watch : remove) {
inlineWatches.get(watch.getPosition().getFile().getUrl()).remove(watch);
}
removeFromStorage(remove);
getWatchesViews().forEach(view -> view.removeInlineWatches(remove));
}
private void removeFromStorage(Collection<InlineWatch> remove) {
for (InlineWatch watch : remove) {
inlineWatches.remove(watch.getPosition().getFile(), watch);
}
}
private Stream<XInlineWatchesView> getWatchesViews() {
return StreamEx.of(XDebugManagerProxy.getInstance().getSessions(myProject))
.map(XDebugSessionProxy::getSessionTab)
@@ -257,10 +256,7 @@ public final class XDebuggerWatchesManagerImpl implements XDebuggerWatchesManage
public @NotNull Collection<InlineWatch> getDocumentInlines(@NotNull Document document) {
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file != null) {
Set<InlineWatch> inlineWatches = this.inlineWatches.get(file.getUrl());
if (inlineWatches != null) {
return new ArrayList<>(inlineWatches);
}
return List.copyOf(inlineWatches.get(file));
}
return Collections.emptyList();
}
@@ -266,23 +266,26 @@ class XBreakpointVisualRepresentation(
fun createBreakpointDraggableObject(): GutterDraggableObject {
return object : GutterDraggableObject {
override fun copy(line: Int, file: VirtualFile?, actionId: Int): Boolean {
if (canMoveTo(line, file)) {
if (file != null && canMoveTo(line, file)) {
// TODO IJPL-185322 implement DnD for light breakpoints?
if (myBreakpoint !is XLineBreakpointProxy) {
return false
}
val breakpointManager = XDebugManagerProxy.getInstance().getBreakpointManagerProxy(myProject)
if (isCopyAction(actionId)) {
breakpointManager.copyLineBreakpoint(myBreakpoint, file!!, line)
breakpointManager.copyLineBreakpoint(myBreakpoint, file, line)
}
else {
myBreakpoint.setFileUrl(file!!.url)
myBreakpoint.setLine(line)
val sessionProxy = XDebugManagerProxy.getInstance().getCurrentSessionProxy(myProject)
if (sessionProxy != null) {
breakpointManager.onBreakpointRemoval(myBreakpoint, sessionProxy)
val cs = myProject.service<BreakpointDraggableObjectScopeProvider>().cs
cs.launch { // switch to avoid blocking url call on EDT
myBreakpoint.setFileUrl(file.url)
myBreakpoint.setLine(line)
val sessionProxy = XDebugManagerProxy.getInstance().getCurrentSessionProxy(myProject)
if (sessionProxy != null) {
breakpointManager.onBreakpointRemoval(myBreakpoint, sessionProxy)
}
DebuggerUIUtil.notifyBreakpointAttachments(myBreakpoint)
}
DebuggerUIUtil.notifyBreakpointAttachments(myBreakpoint)
return true
}
}
@@ -336,6 +339,9 @@ class XBreakpointVisualRepresentation(
}
}
@Service(Service.Level.PROJECT)
private class BreakpointDraggableObjectScopeProvider(val cs: CoroutineScope)
@Service(Service.Level.PROJECT)
private class RedrawInlaysService(private val cs: CoroutineScope) {
private val limitedDispatcher = Dispatchers.Default.limitedParallelism(1)
@@ -46,7 +46,7 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.registry.RegistryValue
import com.intellij.openapi.util.registry.RegistryValueListener
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.platform.debugger.impl.rpc.XBreakpointId
import com.intellij.platform.debugger.impl.shared.proxy.XBreakpointManagerProxy
import com.intellij.platform.debugger.impl.shared.proxy.XBreakpointProxy
import com.intellij.platform.debugger.impl.shared.proxy.XLightLineBreakpointProxy
@@ -76,8 +76,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.awt.event.MouseEvent
import java.util.concurrent.ConcurrentHashMap
private val log = logger<XLineBreakpointManager>()
@@ -90,7 +90,9 @@ class XLineBreakpointManager(
): XLineBreakpointManagerProxy {
private val cs = coroutineScope.childScope("XLineBreakpointManager")
private val myBreakpoints = MultiMap.createConcurrent<String, XLineBreakpointProxy>()
private val myBreakpointsById = ConcurrentHashMap<XBreakpointId, XLineBreakpointProxy>()
private val myBreakpointsByFile = MultiMap.createConcurrent<VirtualFile, XLineBreakpointProxy>()
private val breakpointUpdateQueue: MergingUpdateQueue = MergingUpdateQueue.mergingUpdateQueue(
name = "XLine breakpoints",
mergingTimeSpan = 300,
@@ -114,8 +116,7 @@ class XLineBreakpointManager(
Registry.get(XDebuggerUtil.INLINE_BREAKPOINTS_KEY).addListener(object : RegistryValueListener {
override fun afterValueChanged(value: RegistryValue) {
for (fileUrl in myBreakpoints.keySet()) {
val file = VirtualFileManager.getInstance().findFileByUrl(fileUrl) ?: continue
for (file in myBreakpointsByFile.keySet()) {
if (XDebuggerUtil.areInlineBreakpointsEnabled(file)) continue
val document = FileDocumentManager.getInstance().getDocument(file) ?: continue
// Multiple breakpoints on the single line should be joined in this case.
@@ -129,7 +130,7 @@ class XLineBreakpointManager(
busConnection.subscribe(EditorColorsManager.TOPIC, MyEditorColorsListener())
busConnection.subscribe(FileDocumentManagerListener.TOPIC, object : FileDocumentManagerListener {
override fun fileContentLoaded(file: VirtualFile, document: Document) {
myBreakpoints[file.url].asSequence()
getFileBreakpoints(file).asSequence()
.filter { it.getHighlighter() == null }
.forEach { queueBreakpointUpdate(it) }
}
@@ -140,15 +141,13 @@ class XLineBreakpointManager(
}
}
@ApiStatus.Internal
fun onFileDeleted(url: String) {
removeBreakpoints(myBreakpoints[url])
fun onFileDeleted(file: VirtualFile) {
removeBreakpoints(getFileBreakpoints(file))
}
@ApiStatus.Internal
fun onFileUrlChanged(oldUrl: String, newUrl: String) {
myBreakpoints.values().forEach { breakpoint ->
val url = breakpoint.getFile()?.url ?: breakpoint.getFileUrl()
getAllBreakpoints().forEach { breakpoint ->
val url = breakpoint.getFileUrl()
if (FileUtil.startsWith(url, oldUrl)) {
breakpoint.setFileUrl(newUrl + url.substring(oldUrl.length))
}
@@ -175,25 +174,40 @@ class XLineBreakpointManager(
if (initUI) {
updateBreakpointNow(breakpoint)
}
val fileUrl = breakpoint.getFile()?.url ?: breakpoint.getFileUrl()
log.debug { "Register line breakpoint ${breakpoint.id} ${breakpoint.javaClass.simpleName}: $fileUrl" }
myBreakpoints.putValue(fileUrl, breakpoint)
myBreakpointsById[breakpoint.id] = breakpoint
log.debug { "Register line breakpoint ${breakpoint.id} ${breakpoint.javaClass.simpleName}: ${breakpoint.getFileUrl()}" }
val file = breakpoint.getFile()
if (file != null) {
myBreakpointsByFile.putValue(file, breakpoint)
}
else {
log.warn("Breakpoint(${breakpoint.id}) file is not found during registration: ${breakpoint.getFileUrl()}")
}
}
fun unregisterBreakpoint(breakpoint: XLineBreakpointProxy) {
val fileUrl = breakpoint.getFile()?.url ?: breakpoint.getFileUrl()
val removed = myBreakpoints.remove(fileUrl, breakpoint)
log.debug { "Unregister line breakpoint ${breakpoint.id} [removed=$removed] ${breakpoint.javaClass.simpleName}: $fileUrl" }
val removed = myBreakpointsById.remove(breakpoint.id) != null
val removedByFile = breakpoint.getFile()?.let { myBreakpointsByFile.remove(it, breakpoint) } ?: false
if (removed != removedByFile) {
val associatedFile = myBreakpointsByFile.entrySet().firstOrNull { it.value == breakpoint }?.key
if (associatedFile != null) {
myBreakpointsByFile.remove(associatedFile, breakpoint)
}
}
log.debug { "Unregister line breakpoint ${breakpoint.id} [removed=$removed] ${breakpoint.javaClass.simpleName}: ${breakpoint.getFileUrl()}" }
}
override fun getDocumentBreakpointProxies(document: Document): Collection<XLineBreakpointProxy> {
val file = FileDocumentManager.getInstance().getFile(document) ?: return emptyList()
return myBreakpoints[file.url]
return getFileBreakpoints(file)
}
@TestOnly
override fun getAllBreakpoints(): Collection<XLineBreakpointProxy> {
return myBreakpoints.values()
return myBreakpointsById.values
}
fun getFileBreakpoints(file: VirtualFile): Collection<XLineBreakpointProxy> {
return myBreakpointsByFile[file]
}
@RequiresEdt
@@ -408,7 +422,7 @@ class XLineBreakpointManager(
fun queueAllBreakpointsUpdate() {
breakpointUpdateQueue.queue(object : Update("all breakpoints") {
override fun run() {
for (breakpoint in myBreakpoints.values()) {
for (breakpoint in getAllBreakpoints()) {
breakpoint.doUpdateUI()
}
}