diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/LowMemoryWatcher.java b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/LowMemoryWatcher.java index d41af907bb6..ebb0ef981ec 100644 --- a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/LowMemoryWatcher.java +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/LowMemoryWatcher.java @@ -19,7 +19,9 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.containers.WeakList; +import org.jetbrains.annotations.NotNull; +import javax.management.ListenerNotFoundException; import javax.management.Notification; import javax.management.NotificationEmitter; import javax.management.NotificationListener; @@ -33,9 +35,10 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** - * @author Eugene Zhuravlev - * Date: Aug 24, 2010 + * PATCHED VERSION of com.intellij.openapi.util.LowMemoryWatcher + * shutdown() method added to stop background thread. */ +@SuppressWarnings("UnusedDeclaration") public class LowMemoryWatcher { private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024; @@ -68,6 +71,8 @@ public class LowMemoryWatcher { private final Runnable myRunnable; + private static final NotificationListener lowMemoryListener; + static { for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) { if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) { @@ -78,10 +83,12 @@ public class LowMemoryWatcher { } } } - ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(new NotificationListener() { + + lowMemoryListener = new NotificationListener() { @Override - public void handleNotification(Notification n, Object hb) { - if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(n.getType()) || MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(n.getType())) { + public void handleNotification(@NotNull Notification n, Object hb) { + if (MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(n.getType()) || + MemoryNotificationInfo.MEMORY_COLLECTION_THRESHOLD_EXCEEDED.equals(n.getType())) { synchronized (ourJanitor) { if (!ourSubmitted) { //noinspection AssignmentToStaticFieldFromInstanceMethod @@ -91,7 +98,9 @@ public class LowMemoryWatcher { } } } - }, null, null); + }; + + ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(lowMemoryListener, null, null); } /** @@ -126,4 +135,15 @@ public class LowMemoryWatcher { ourInstances.remove(this); } + public static void shutdown() throws InterruptedException, ListenerNotFoundException { + ourInstances.clear(); + ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).removeNotificationListener(lowMemoryListener); + + ourExecutor.shutdown(); + + boolean terminated = ourExecutor.awaitTermination(1000, TimeUnit.MICROSECONDS); + if (!terminated) { + ourExecutor.shutdownNow(); + } + } } diff --git a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/io/ZipFileCache.java b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/io/ZipFileCache.java index 9480d55c748..5b7b281f854 100644 --- a/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/io/ZipFileCache.java +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/io/ZipFileCache.java @@ -27,30 +27,15 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.zip.ZipFile; /** - *

Utility class which tries to keep frequently requested .zip files open - * to avoid time loss on closing/reopening ZipFile instances.

- * - *

Clients obtain a file by calling {@link #acquire(String)} - * and indicate the loss of interest to it via {@link #release(ZipFile)}. - * Released files are closed after some period of time (about 30 seconds), - * unless requested again within the period.

- * - *

Since ZipFiles are read-only objects allowing concurrent access, - * a same instance may be returned to a different threads requesting a same path. - * A file may be closed only after being released by all applicants.

- * - *

The class does not expect .zip files on a disk to be changed, - * so it may return an outdated instance of ZipFile (reading from it - * may return inaccurate data or even cause an exceptions to happen). - * It's a clients' responsibility to keep a track of .zip files - * and call the {@link #reset(Collection)} method for a paths - * which are possibly changed. Reset paths are removed from the cache - * and are closed immediately after being released.

+ * PATCHED VERSION of com.intellij.openapi.util.io.ZipFileCache + * shutdown() method added to stop background thread. */ +@SuppressWarnings("UnusedDeclaration") public class ZipFileCache { private static final int PERIOD = 10000; // disposer schedule, ms private static final int TIMEOUT = 30000; // released file close delay, ms @@ -72,8 +57,12 @@ public class ZipFileCache { private static final Map ourFileCache = ContainerUtil.newHashMap(); private static final Map ourQueue = ContainerUtil.newHashMap(); + private static final ScheduledThreadPoolExecutor scheduledCloseExecutor; + static { - ConcurrencyUtil.newSingleScheduledThreadExecutor("ZipFileCache Dispose", Thread.MIN_PRIORITY).scheduleWithFixedDelay(new Runnable() { + scheduledCloseExecutor = + ConcurrencyUtil.newSingleScheduledThreadExecutor("ZipFileCache Dispose", Thread.MIN_PRIORITY); + scheduledCloseExecutor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { List toClose = getFilesToClose(0, System.currentTimeMillis() - TIMEOUT); @@ -236,4 +225,13 @@ public class ZipFileCache { private static void debug(@NotNull String format, Object... args) { LogUtil.debug(logger(), format, args); } -} \ No newline at end of file + + public static void shutdown() throws InterruptedException { + scheduledCloseExecutor.shutdown(); + + boolean terminated = scheduledCloseExecutor.awaitTermination(TIMEOUT, TimeUnit.MICROSECONDS); + if (!terminated) { + scheduledCloseExecutor.shutdownNow(); + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/FinishBuildListner.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/FinishBuildListner.kt new file mode 100644 index 00000000000..c3b5d8dda80 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/FinishBuildListner.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2010-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.gradle.plugin + +import org.gradle.BuildAdapter +import org.gradle.api.logging.Logging +import org.gradle.BuildResult +import java.lang.ref.Reference +import java.lang.reflect.Array +import kotlin.platform.platformStatic + +class FinishBuildListener(var pluginClassLoader: ParentLastURLClassLoader?) : BuildAdapter() { + val log = Logging.getLogger(this.javaClass) + + class object { + platformStatic + fun isRequestedClass(fqName: String): Boolean { + return when (fqName) { + "com.intellij.openapi.util.io.ZipFileCache", + "com.intellij.openapi.util.LowMemoryWatcher" -> true + else -> false + } + } + } + + override fun buildFinished(result: BuildResult?) { + log.debug("Build finished listener") + + stopZipFileCache() + stopLowMemoryWatcher() + + // TODO: Try to clean up thread locals without this ugly hack + // TODO: Further investigation of PermGen leak (KT-6451) + removeThreadLocals() + + pluginClassLoader = null + result?.getGradle()?.removeListener(this) + } + + public fun removeThreadLocals() { + try { + log.debug("Remove ChildURLClassLoader thread locals") + + val thread = Thread.currentThread() + val threadLocalsField = javaClass().getDeclaredField("threadLocals") + threadLocalsField.setAccessible(true) + + val threadLocalMapClass = Class.forName("java.lang.ThreadLocal\$ThreadLocalMap") + val tableField = threadLocalMapClass.getDeclaredField("table") + tableField.setAccessible(true) + + val referentField = javaClass>().getDeclaredField("referent") + referentField.setAccessible(true) + + val table = tableField[threadLocalsField[thread]] as kotlin.Array<*> + + for (entry in table) { + if (entry != null) { + val threadLocal = referentField[entry] as ThreadLocal<*>? + val classLoader = threadLocal?.javaClass?.getClassLoader() + if (classLoader is ParentLastURLClassLoader.ChildURLClassLoader) { + threadLocal?.remove() + } + } + } + + log.debug("Removing ChildURLClassLoader thread locals finished successfully") + } catch (e: Throwable) { + log.debug("Exception during thread locals remove: " + e) + } + } + + private fun stopZipFileCache() { + callVoidStaticMethod("com.intellij.openapi.util.io.ZipFileCache", "shutdown") + log.debug("ZipFileCache finished successfully") + } + + private fun stopLowMemoryWatcher() { + callVoidStaticMethod("com.intellij.openapi.util.LowMemoryWatcher", "shutdown") + log.debug("LowMemoryWatcher finished successfully") + } + + private fun callVoidStaticMethod(classFqName: String, methodName: String) { + val shortName = classFqName.substring(classFqName.lastIndexOf('.') + 1) + + log.debug("Looking for $shortName class") + val lowMemoryWatcherClass = Class.forName(classFqName, false, pluginClassLoader) + + log.debug("Looking for $methodName() method") + val shutdownMethod = lowMemoryWatcherClass.getMethod(methodName) + + log.debug("Call $shortName.$methodName()") + shutdownMethod.invoke(null) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index 326313c7c56..6eadc4ee21a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -4,21 +4,14 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.ConfigurationContainer -import org.gradle.api.specs.Spec -import java.io.File import java.net.URL import org.gradle.api.logging.Logging import java.util.Properties import java.io.FileNotFoundException import org.gradle.api.initialization.dsl.ScriptHandler -import java.lang.reflect.Method +import org.gradle.api.invocation.Gradle abstract class KotlinBasePluginWrapper: Plugin { - - class object { - val pluginVersionsMap: MutableMap>> = hashMapOf() - } - val log = Logging.getLogger(this.javaClass) public override fun apply(project: Project) { @@ -33,7 +26,10 @@ abstract class KotlinBasePluginWrapper: Plugin { val kotlinPluginVersion = loadKotlinVersionFromResource() project.getExtensions().getExtraProperties()?.set("kotlin.gradle.plugin.version", kotlinPluginVersion) - val cls = pluginVersionsMap.getOrElse("$kotlinPluginVersion:${getPluginClassName()}", { loadPluginInIsolatedClassloader(kotlinPluginVersion, sourceBuildScript) }) + val pluginClassLoader = createPluginIsolatedClassLoader(kotlinPluginVersion, sourceBuildScript) + + val cls = Class.forName(getPluginClassName(), true, pluginClassLoader) + log.debug("Plugin class loaded") val constructor = cls.getConstructor(javaClass()) val method = cls.getMethod("apply", javaClass()) @@ -44,9 +40,12 @@ abstract class KotlinBasePluginWrapper: Plugin { method?.invoke(plugin, project) log.debug("'apply' method invoked successfully") + + val gradle = project.javaClass.getMethod("getGradle").invoke(project) as Gradle + gradle.addBuildListener(FinishBuildListener(pluginClassLoader)) } - private fun loadPluginInIsolatedClassloader(projectVersion: String, sourceBuildScript: ScriptHandler): Class> { + private fun createPluginIsolatedClassLoader(projectVersion: String, sourceBuildScript: ScriptHandler): ParentLastURLClassLoader { val dependencyHandler: DependencyHandler = sourceBuildScript.getDependencies() val configurationsContainer: ConfigurationContainer = sourceBuildScript.getConfigurations() @@ -61,9 +60,8 @@ abstract class KotlinBasePluginWrapper: Plugin { log.debug("Load plugin in parent-last URL classloader") val kotlinPluginClassloader = ParentLastURLClassLoader(kotlinPluginDependencies, this.javaClass.getClassLoader()) log.debug("Class loader created") - val cls = Class.forName(getPluginClassName(), true, kotlinPluginClassloader) as Class> - log.debug("Plugin class loaded") - return cls + + return kotlinPluginClassloader } private fun loadKotlinVersionFromResource(): String { @@ -113,4 +111,4 @@ open class KotlinAndriodPluginWrapper: KotlinBasePluginWrapper() { public override fun getPluginClassName():String { return "org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPlugin" } -} +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java index dec931c3baa..8f5b9525a3f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/ParentLastURLClassLoader.java @@ -1,9 +1,13 @@ package org.jetbrains.kotlin.gradle.plugin; +import org.jetbrains.annotations.NotNull; + import java.net.URL; import java.net.URLClassLoader; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A parent-last classloader that will try the child classloader first and then the parent. @@ -23,7 +27,7 @@ public class ParentLastURLClassLoader extends ClassLoader { } @Override - protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + protected synchronized Class loadClass(@NotNull String name, boolean resolve) throws ClassNotFoundException { try { // first we try to find a class inside the child classloader return childClassLoader.findClass(name); @@ -41,8 +45,9 @@ public class ParentLastURLClassLoader extends ClassLoader { super(parent); } + @NotNull @Override - public Class findClass(String name) throws ClassNotFoundException { + public Class findClass(@NotNull String name) throws ClassNotFoundException { return super.findClass(name); } } @@ -51,7 +56,9 @@ public class ParentLastURLClassLoader extends ClassLoader { * This class delegates (child then parent) for the findClass method for a URLClassLoader. * We need this because findClass is protected in URLClassLoader */ - private static class ChildURLClassLoader extends URLClassLoader { + public static class ChildURLClassLoader extends URLClassLoader { + private final Map> cache = new HashMap>(); + private FindClassClassLoader realParent; public ChildURLClassLoader(URL[] urls, FindClassClassLoader realParent) { @@ -60,10 +67,22 @@ public class ParentLastURLClassLoader extends ClassLoader { this.realParent = realParent; } + + @NotNull @Override - public Class findClass(String name) throws ClassNotFoundException { + public Class findClass(@NotNull String name) throws ClassNotFoundException { try { - // first try to use the URLClassLoader findClass + // Replace with FinishBuildListener.isRequestedClass(name) after rewriting this class on Kotlin + if (name.equals("com.intellij.openapi.util.io.ZipFileCache") || + name.equals("com.intellij.openapi.util.LowMemoryWatcher")) { + if (cache.containsKey(name)) return cache.get(name); + + Class aClass = super.findClass(name); + cache.put(name, aClass); + + return aClass; + } + return super.findClass(name); } catch (ClassNotFoundException e) { // if that fails, we ask our real parent classloader to load the class (we give up)