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 new file mode 100644 index 00000000000..d41af907bb6 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/LowMemoryWatcher.java @@ -0,0 +1,129 @@ +/* + * Copyright 2000-2013 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 com.intellij.openapi.util; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.containers.WeakList; + +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryNotificationInfo; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @author Eugene Zhuravlev + * Date: Aug 24, 2010 + */ +public class LowMemoryWatcher { + private static final long MEM_THRESHOLD = 5 /*MB*/ * 1024 * 1024; + + private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.LowMemoryWatcher"); + + private static final List ourInstances = new WeakList(); + private static final ThreadPoolExecutor ourExecutor = new ThreadPoolExecutor(0, 1, 10, TimeUnit.SECONDS, new ArrayBlockingQueue(2), ConcurrencyUtil.newNamedThreadFactory("LowMemoryWatcher janitor")); + private static boolean ourSubmitted; + private static final Runnable ourJanitor = new Runnable() { + @Override + public void run() { + try { + for (LowMemoryWatcher watcher : ourInstances) { + try { + watcher.myRunnable.run(); + } + catch (Throwable e) { + LOG.info(e); + } + } + } + finally { + synchronized (ourJanitor) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + ourSubmitted = false; + } + } + } + }; + + private final Runnable myRunnable; + + static { + for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) { + if (bean.getType() == MemoryType.HEAP && bean.isUsageThresholdSupported()) { + long threshold = bean.getUsage().getMax() - MEM_THRESHOLD; + if (threshold > 0) { + bean.setUsageThreshold(threshold); + bean.setCollectionUsageThreshold(threshold); + } + } + } + ((NotificationEmitter)ManagementFactory.getMemoryMXBean()).addNotificationListener(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())) { + synchronized (ourJanitor) { + if (!ourSubmitted) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + ourSubmitted = true; + ourExecutor.submit(ourJanitor); + } + } + } + } + }, null, null); + } + + /** + * Registers a runnable to run on low memory events + * @return a LowMemoryWatcher instance holding the runnable. This instance should be kept in memory while the + * low memory notification functionality is needed. As soon as it's garbage-collected, the runnable won't receive any further notifications. + */ + public static LowMemoryWatcher register(Runnable runnable) { + return new LowMemoryWatcher(runnable); + } + + /** + * Registers a runnable to run on low memory events. The notifications will be issued until parentDisposable is disposed. + */ + public static void register(Runnable runnable, Disposable parentDisposable) { + final Ref watcher = Ref.create(new LowMemoryWatcher(runnable)); + Disposer.register(parentDisposable, new Disposable() { + @Override + public void dispose() { + watcher.get().stop(); + watcher.set(null); + } + }); + } + + private LowMemoryWatcher(Runnable runnable) { + myRunnable = runnable; + ourInstances.add(this); + } + + public void stop() { + ourInstances.remove(this); + } + +} 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 new file mode 100644 index 00000000000..9480d55c748 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-core/src/main/kotlin/com/intellij/openapi/util/io/ZipFileCache.java @@ -0,0 +1,239 @@ +/* + * Copyright 2000-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 com.intellij.openapi.util.io; + +import com.intellij.openapi.diagnostic.LogUtil; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +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.

+ */ +public class ZipFileCache { + private static final int PERIOD = 10000; // disposer schedule, ms + private static final int TIMEOUT = 30000; // released file close delay, ms + + private static class CacheRecord { + private final String path; + private final ZipFile file; + private int count = 1; + private long released = 0; + + private CacheRecord(@NotNull String path, @NotNull ZipFile file) throws IOException { + this.path = path; + this.file = file; + } + } + + private static final Object ourLock = new Object(); + private static final Map ourPathCache = ContainerUtil.newTroveMap(FileUtil.PATH_HASHING_STRATEGY); + private static final Map ourFileCache = ContainerUtil.newHashMap(); + private static final Map ourQueue = ContainerUtil.newHashMap(); + + static { + ConcurrencyUtil.newSingleScheduledThreadExecutor("ZipFileCache Dispose", Thread.MIN_PRIORITY).scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + List toClose = getFilesToClose(0, System.currentTimeMillis() - TIMEOUT); + if (toClose != null) { + close(toClose); + } + } + }, PERIOD, PERIOD, TimeUnit.MILLISECONDS); + } + + @NotNull + public static ZipFile acquire(@NotNull String path) throws IOException { + path = FileUtil.toCanonicalPath(path); + + synchronized (ourLock) { + CacheRecord record = ourPathCache.get(path); + if (record != null) { + record.count++; + return record.file; + } + } + + CacheRecord record; + ZipFile file = tryOpen(path); + + synchronized (ourLock) { + record = ourPathCache.get(path); + if (record == null) { + record = new CacheRecord(path, file); + ourPathCache.put(path, record); + ourFileCache.put(file, record); + return file; + } + else { + record.count++; + } + } + + close(file); + return record.file; + } + + private static ZipFile tryOpen(String path) throws IOException { + path = FileUtil.toSystemDependentName(path); + debug("opening %s", path); + try { + return new ZipFile(path); + } + catch (IOException e) { + String reason = e.getMessage(); + if ("too many open files".equalsIgnoreCase(reason) && tryCloseFiles() > 0) { + return new ZipFile(path); + } + else { + throw e; + } + } + } + + private static int tryCloseFiles() { + List toClose = getFilesToClose(5, 0); + if (toClose == null) return 0; + close(toClose); + logger().warn("too many open files, closed: " + toClose.size()); + return toClose.size(); + } + + @Nullable + private static List getFilesToClose(int limit, long timeout) { + List toClose = null; + + synchronized (ourLock) { + Iterator i = ourPathCache.values().iterator(); + while (i.hasNext() && (limit == 0 || toClose == null || toClose.size() < limit)) { + CacheRecord record = i.next(); + if (record.count <= 0 && (timeout == 0 || record.released <= timeout)) { + i.remove(); + ourFileCache.remove(record.file); + if (toClose == null) toClose = ContainerUtil.newArrayList(); + toClose.add(record.file); + } + } + } + + return toClose; + } + + public static void release(@NotNull ZipFile file) { + synchronized (ourLock) { + CacheRecord record = ourFileCache.get(file); + if (record != null) { + record.count--; + record.released = System.currentTimeMillis(); + logger().assertTrue(record.count >= 0, record.path); + return; + } + + Integer count = ourQueue.get(file); + if (count != null) { + count--; + if (count == 0) { + ourQueue.remove(file); + close(file); + } + else { + ourQueue.put(file, count); + } + return; + } + } + + logger().warn(new IllegalArgumentException("stray file: " + file.getName())); + close(file); + } + + public static void reset(@NotNull Collection paths) { + debug("resetting %s", paths); + + List toClose = ContainerUtil.newSmartList(); + + synchronized (ourLock) { + for (String path : paths) { + path = FileUtil.toCanonicalPath(path); + CacheRecord record = ourPathCache.remove(path); + if (record != null) { + ourFileCache.remove(record.file); + if (record.count > 0) { + ourQueue.put(record.file, record.count); + } + else { + toClose.add(record.file); + } + } + } + } + + close(toClose); + } + + private static void close(@NotNull List files) { + for (ZipFile file : files) { + close(file); + } + } + + private static void close(@NotNull ZipFile file) { + debug("closing %s", file.getName()); + try { + file.close(); + } + catch (IOException e) { + logger().info(file.getName(), e); + } + } + + private static Logger logger() { + return Logger.getInstance(ZipFileCache.class); + } + + private static void debug(@NotNull String format, Object... args) { + LogUtil.debug(logger(), format, args); + } +} \ No newline at end of file