From 84bdfdcbf457b5ca8f561c0fe9f98a9be189b9ed Mon Sep 17 00:00:00 2001 From: viuginick Date: Wed, 28 Aug 2024 13:37:53 +0000 Subject: [PATCH] An ability to ignore well-known leaks in LeakHunter added It allows to keep the LeakHunter-based checks enabled while "skipping" some well-known leaks. (cherry picked from commit 4616d58b9e1b8de1b26ce5e9b566aca10bb864c7) --- .../util/ref/DebugReflectionUtil.java | 11 +++-- .../util/ref/IgnoredTraverseEntry.java | 8 ++++ .../util/ref/IgnoredTraverseReference.java | 48 +++++++++++++++++++ .../intellij/openapi/application/impl/util.kt | 2 +- .../testFramework/common/src/LeakHunter.java | 35 +++++++++++++- .../common/src/common/testApplication.kt | 9 +++- .../intellij/project/TestProjectManager.kt | 1 + .../testFramework/TestApplicationManager.kt | 12 ++++- 8 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseEntry.java create mode 100644 platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseReference.java diff --git a/platform/core-impl/src/com/intellij/util/ref/DebugReflectionUtil.java b/platform/core-impl/src/com/intellij/util/ref/DebugReflectionUtil.java index 4284ec93bfefb..8c4c11a6d817c 100644 --- a/platform/core-impl/src/com/intellij/util/ref/DebugReflectionUtil.java +++ b/platform/core-impl/src/com/intellij/util/ref/DebugReflectionUtil.java @@ -229,7 +229,7 @@ public static class BackLink { * when null, it can be computed from field.getName() */ private final String fieldName; - private final BackLink backLink; + final BackLink backLink; private final int depth; BackLink(@NotNull V value, @Nullable Field field, @Nullable String fieldName, @Nullable BackLink backLink) { @@ -252,6 +252,10 @@ public String toString() { return result.toString(); } + String getFieldName() { + return this.fieldName != null ? this.fieldName : field.getDeclaringClass().getName() + "." + field.getName(); + } + void print(@NotNull StringBuilder result) { String valueStr; Object value = this.value; @@ -270,9 +274,8 @@ void print(@NotNull StringBuilder result) { valueStr = "(" + e.getMessage() + " while computing .toString())"; } - Field field = this.field; - String fieldName = this.fieldName != null ? this.fieldName : field.getDeclaringClass().getName() + "." + field.getName(); - result.append("via '").append(fieldName).append("'; Value: '").append(valueStr).append("' of ").append(value.getClass()).append("\n"); + result.append("via '").append(getFieldName()).append("'; Value: '").append(valueStr).append("' of ").append(value.getClass()) + .append("\n"); } } } diff --git a/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseEntry.java b/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseEntry.java new file mode 100644 index 0000000000000..39a208325aace --- /dev/null +++ b/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseEntry.java @@ -0,0 +1,8 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.util.ref; + +import java.util.function.Predicate; + +public interface IgnoredTraverseEntry extends + Predicate> { +} diff --git a/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseReference.java b/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseReference.java new file mode 100644 index 0000000000000..35e48429dce66 --- /dev/null +++ b/platform/core-impl/src/com/intellij/util/ref/IgnoredTraverseReference.java @@ -0,0 +1,48 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.util.ref; + +import org.jetbrains.annotations.NotNull; + +public class IgnoredTraverseReference implements IgnoredTraverseEntry { + + + private final String myField; + private final int myIndex; + + /** + * Constructs a rule that allows to ignore some known leakage path. It allows to keep the LeakHunter-based checks enabled while "skipping" + * some well-known leaks. + * + * @param field field name in the CLASS_FQN.field_name format + * @param index index of the {@param field} frame. Index can be positive and negative. If it's positive it will be enumerated starting + * from the traverse root, from the traverse leaf if it's negative. + */ + public IgnoredTraverseReference(@NotNull final String field, int index) { + myField = field; + myIndex = index; + } + + @Override + public boolean test(@NotNull final DebugReflectionUtil.BackLink link) { + if (myIndex >= 0) { + // TODO + return false; + } + return checkNegativeIndex(link); + } + + private boolean checkNegativeIndex(@NotNull final DebugReflectionUtil.BackLink link) { + int currentIndex = myIndex; + DebugReflectionUtil.BackLink backLink = link; + + while (currentIndex < -1) { + backLink = backLink.backLink; + currentIndex++; + if (backLink == null) { + return false; + } + } + + return myField.equals(backLink.getFieldName()); + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt index e3d8a82930781..2798e49ad0d17 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt +++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt @@ -38,7 +38,7 @@ fun assertReferenced(root: Any, referenced: Any) { val rootSupplier: Supplier> = Supplier { mapOf(root to "root") } - LeakHunter.processLeaks(rootSupplier, referenced.javaClass, null) { leaked, _ -> + LeakHunter.processLeaks(rootSupplier, referenced.javaClass, null, null) { leaked, _ -> foundObjects.add(leaked) true } diff --git a/platform/testFramework/common/src/LeakHunter.java b/platform/testFramework/common/src/LeakHunter.java index fefcfdd1a8e96..ebda8a95c87cd 100644 --- a/platform/testFramework/common/src/LeakHunter.java +++ b/platform/testFramework/common/src/LeakHunter.java @@ -21,6 +21,7 @@ import com.intellij.util.io.PersistentEnumeratorCache; import com.intellij.util.ref.DebugReflectionUtil; import com.intellij.util.ref.GCUtil; +import com.intellij.util.ref.IgnoredTraverseEntry; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -49,6 +50,11 @@ public static void checkNonDefaultProjectLeak() { checkLeak(allRoots(), ProjectImpl.class, project -> !project.isDefault()); } + @TestOnly + public static void checkNonDefaultProjectLeakWithIgnoredEntries(@NotNull List ignoredTraverseEntries) { + checkLeak(allRoots(), ProjectImpl.class, ignoredTraverseEntries, project -> !project.isDefault()); + } + @TestOnly public static void checkLeak(@NotNull Object root, @NotNull Class suspectClass) throws AssertionError { checkLeak(root, suspectClass, null); @@ -61,7 +67,30 @@ public static void checkLeak(@NotNull Object root, @NotNull Class suspectClas public static void checkLeak(@NotNull Supplier> rootsSupplier, @NotNull Class suspectClass, @Nullable Predicate isReallyLeak) throws AssertionError { - processLeaks(rootsSupplier, suspectClass, isReallyLeak, (leaked, backLink)->{ + processLeaks(rootsSupplier, suspectClass, isReallyLeak, null, (leaked, backLink)->{ + String message = getLeakedObjectDetails(leaked, backLink, true); + + System.out.println(message); + System.out.println(";-----"); + ThreadUtil.printThreadDump(); + + throw new AssertionError(message); + }); + } + + @TestOnly + public static void checkLeak(@NotNull Supplier> rootsSupplier, + @NotNull Class suspectClass, + @NotNull List ignoredTraverseEntries, + @Nullable Predicate isReallyLeak) throws AssertionError { + processLeaks(rootsSupplier, suspectClass, isReallyLeak, (backLink) -> { + for (IgnoredTraverseEntry entry : ignoredTraverseEntries) { + if (entry.test(backLink)) { + return true; + } + } + return false; + }, (leaked, backLink) -> { String message = getLeakedObjectDetails(leaked, backLink, true); System.out.println(message); @@ -79,6 +108,7 @@ public static void checkLeak(@NotNull Supplier public static void processLeaks(@NotNull Supplier> rootsSupplier, @NotNull Class suspectClass, @Nullable Predicate isReallyLeak, + @Nullable Predicate> leakBackLinkProcessor, @NotNull PairProcessor processor) throws AssertionError { if (SwingUtilities.isEventDispatchThread()) { UIUtil.dispatchAllInvocationEvents(); @@ -92,6 +122,9 @@ public static void processLeaks(@NotNull Supplier { try (AccessToken ignored = ProhibitAWTEvents.start("checking for leaks")) { DebugReflectionUtil.walkObjects(10000, rootsSupplier.get(), suspectClass, __ -> true, (leaked, backLink) -> { + if (leakBackLinkProcessor != null && leakBackLinkProcessor.test(backLink)) { + return true; + } if (isReallyLeak == null || isReallyLeak.test(leaked)) { return processor.process(leaked, backLink); } diff --git a/platform/testFramework/common/src/common/testApplication.kt b/platform/testFramework/common/src/common/testApplication.kt index 2fb818df376d9..b186203bedcb6 100644 --- a/platform/testFramework/common/src/common/testApplication.kt +++ b/platform/testFramework/common/src/common/testApplication.kt @@ -66,6 +66,7 @@ import com.intellij.util.WalkingState import com.intellij.util.concurrency.AppScheduledExecutorService import com.intellij.util.indexing.FileBasedIndex import com.intellij.util.indexing.FileBasedIndexImpl +import com.intellij.util.ref.IgnoredTraverseEntry import com.intellij.util.ui.EDT import com.intellij.util.ui.EdtInvocationManager import com.jetbrains.JBR @@ -326,8 +327,14 @@ fun Application.cleanupApplicationCaches() { @TestOnly @Internal fun assertNonDefaultProjectsAreNotLeaked() { + assertNonDefaultProjectsAreNotLeaked(emptyList()) +} + +@TestOnly +@Internal +fun assertNonDefaultProjectsAreNotLeaked(ignoredTraverseEntries : List) { try { - LeakHunter.checkNonDefaultProjectLeak() + LeakHunter.checkNonDefaultProjectLeakWithIgnoredEntries(ignoredTraverseEntries) } catch (e: AssertionError) { publishHeapDump(LEAKED_PROJECTS) diff --git a/platform/testFramework/src/com/intellij/project/TestProjectManager.kt b/platform/testFramework/src/com/intellij/project/TestProjectManager.kt index 20c3f56c532f3..0d4eb2a50a770 100644 --- a/platform/testFramework/src/com/intellij/project/TestProjectManager.kt +++ b/platform/testFramework/src/com/intellij/project/TestProjectManager.kt @@ -278,6 +278,7 @@ private fun reportLeakedProjects(leakedProjects: Iterable) { val dumpPath = publishHeapDump(LEAKED_PROJECTS) LeakHunter.processLeaks(LeakHunter.allRoots(), ProjectImpl::class.java, { hashCodes.contains(System.identityHashCode(it)) }, + null, { leaked, backLink -> val hashCode = System.identityHashCode(leaked) message += LeakHunter.getLeakedObjectDetails(leaked, backLink, false) diff --git a/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt b/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt index 15adf3e4b2f11..b901892653a66 100644 --- a/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt +++ b/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt @@ -15,7 +15,10 @@ import com.intellij.ide.structureView.StructureViewFactory import com.intellij.ide.structureView.impl.StructureViewFactoryImpl import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.application.* +import com.intellij.openapi.application.Application +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.application.WriteIntentReadAction import com.intellij.openapi.application.impl.ApplicationImpl import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.command.impl.UndoManagerImpl @@ -39,6 +42,7 @@ import com.intellij.testFramework.common.* import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.concurrency.AppScheduledExecutorService import com.intellij.util.ref.GCUtil +import com.intellij.util.ref.IgnoredTraverseEntry import com.intellij.util.ui.EDT import com.intellij.util.ui.UIUtil import com.intellij.workspaceModel.ide.impl.legacyBridge.module.roots.ModuleRootComponentBridge @@ -179,6 +183,10 @@ class TestApplicationManager private constructor() { */ @JvmStatic fun disposeApplicationAndCheckForLeaks() { + disposeApplicationAndCheckForLeaks(emptyList()) + } + @JvmStatic + fun disposeApplicationAndCheckForLeaks(ignoredTraverseEntries : List) { val edtThrowable = runInEdtAndGet { runAllCatching( { PlatformTestUtil.cleanupAllProjects() }, @@ -194,7 +202,7 @@ class TestApplicationManager private constructor() { { UsefulTestCase.waitForAppLeakingThreads(10, TimeUnit.SECONDS) }, { if (ApplicationManager.getApplication() != null) { - assertNonDefaultProjectsAreNotLeaked() + assertNonDefaultProjectsAreNotLeaked(ignoredTraverseEntries) } }, {