/* * 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.project; import com.intellij.ide.IdeBundle; import com.intellij.ide.caches.CacheUpdater; import com.intellij.ide.caches.FileContent; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationAdapter; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.impl.ApplicationImpl; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.util.ProgressIndicatorBase; import com.intellij.openapi.progress.util.ProgressWrapper; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; public class CacheUpdateRunner extends DumbModeTask { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.project.CacheUpdateRunner"); private static final Key FAILED_TO_INDEX = Key.create("FAILED_TO_INDEX"); private static final int PROC_COUNT = Runtime.getRuntime().availableProcessors(); private final Project myProject; private final Collection myUpdaters; private CacheUpdateSession mySession; CacheUpdateRunner(@NotNull Project project, @NotNull Collection updaters) { myProject = project; myUpdaters = updaters; } @Override public String toString() { return new ArrayList(myUpdaters).toString(); } private int queryNeededFiles(@NotNull ProgressIndicator indicator) { // can be queried twice in DumbService return getSession(indicator).getFilesToUpdate().size(); } @NotNull private CacheUpdateSession getSession(@NotNull ProgressIndicator indicator) { CacheUpdateSession session = mySession; if (session == null) { mySession = session = new CacheUpdateSession(myUpdaters, indicator); } return session; } private void processFiles(@NotNull final ProgressIndicator indicator, boolean processInReadAction) { try { Collection files = mySession.getFilesToUpdate(); processFiles(indicator, processInReadAction, files, myProject, new Consumer() { @Override public void consume(FileContent content) { mySession.processFile(content); } }); } catch (ProcessCanceledException e) { mySession.canceled(); throw e; } } public static void processFiles(final ProgressIndicator indicator, boolean processInReadAction, Collection files, Project project, Consumer processor) { indicator.checkCanceled(); final FileContentQueue queue = new FileContentQueue(); final double total = files.size(); queue.queue(files, indicator); Consumer progressUpdater = new Consumer() { // need set here to handle queue.pushbacks after checkCancelled() in order // not to count the same file several times final Set processed = new THashSet(); @Override public void consume(VirtualFile virtualFile) { indicator.checkCanceled(); synchronized (processed) { processed.add(virtualFile); indicator.setFraction(processed.size() / total); if (ApplicationManager.getApplication().isInternal()) { indicator.setText2(virtualFile.getPresentableUrl()); } } } }; while (!project.isDisposed()) { indicator.checkCanceled(); // todo wait for the user... if (processSomeFilesWhileUserIsInactive(queue, progressUpdater, processInReadAction, project, processor)) { break; } } if (project.isDisposed()) { indicator.cancel(); indicator.checkCanceled(); } } private void updatingDone() { try { mySession.updatingDone(); } catch (ProcessCanceledException e) { mySession.canceled(); throw e; } } private static boolean processSomeFilesWhileUserIsInactive(@NotNull FileContentQueue queue, @NotNull Consumer progressUpdater, final boolean processInReadAction, @NotNull Project project, @NotNull Consumer fileProcessor) { final ProgressIndicatorBase innerIndicator = new ProgressIndicatorBase() { @Override protected boolean isCancelable() { return true; // the inner indicator must be always cancelable } }; final ApplicationAdapter canceller = new ApplicationAdapter() { @Override public void beforeWriteActionStart(Object action) { innerIndicator.cancel(); } }; final Application application = ApplicationManager.getApplication(); application.addApplicationListener(canceller); final AtomicBoolean isFinished = new AtomicBoolean(); try { int threadsCount = Registry.intValue("caches.indexerThreadsCount"); if (threadsCount <= 0) { threadsCount = Math.max(1, Math.min(PROC_COUNT - 1, 4)); } if (threadsCount == 1) { Runnable process = new MyRunnable(innerIndicator, queue, isFinished, progressUpdater, processInReadAction, project, fileProcessor); ProgressManager.getInstance().runProcess(process, innerIndicator); } else { AtomicBoolean[] finishedRefs = new AtomicBoolean[threadsCount]; Future[] futures = new Future[threadsCount]; for (int i = 0; i < threadsCount; i++) { AtomicBoolean ref = new AtomicBoolean(); finishedRefs[i] = ref; Runnable process = new MyRunnable(innerIndicator, queue, ref, progressUpdater, processInReadAction, project, fileProcessor); futures[i] = ApplicationManager.getApplication().executeOnPooledThread(getProcessWrapper(process)); } isFinished.set(waitForAll(finishedRefs, futures)); } } finally { application.removeApplicationListener(canceller); } return isFinished.get(); } private static boolean waitForAll(@NotNull AtomicBoolean[] finishedRefs, @NotNull Future[] futures) { try { for (Future future : futures) { future.get(); } boolean allFinished = true; for (AtomicBoolean ref : finishedRefs) { if (!ref.get()) { allFinished = false; break; } } return allFinished; } catch (InterruptedException ignored) { } catch (Throwable throwable) { LOG.error(throwable); } return false; } @Override public void performInDumbMode(@NotNull ProgressIndicator indicator) { indicator.checkCanceled(); indicator.setIndeterminate(true); indicator.setText(IdeBundle.message("progress.indexing.scanning")); int count = queryNeededFiles(indicator); indicator.setIndeterminate(false); indicator.setText(IdeBundle.message("progress.indexing.updating")); if (count > 0) { processFiles(indicator, true); } updatingDone(); } private static class MyRunnable implements Runnable { private final ProgressIndicatorBase myInnerIndicator; private final FileContentQueue myQueue; private final AtomicBoolean myFinished; private final Consumer myProgressUpdater; private final boolean myProcessInReadAction; @NotNull private final Project myProject; @NotNull private final Consumer myProcessor; public MyRunnable(@NotNull ProgressIndicatorBase innerIndicator, @NotNull FileContentQueue queue, @NotNull AtomicBoolean finished, @NotNull Consumer progressUpdater, boolean processInReadAction, @NotNull Project project, @NotNull Consumer fileProcessor) { myInnerIndicator = innerIndicator; myQueue = queue; myFinished = finished; myProgressUpdater = progressUpdater; myProcessInReadAction = processInReadAction; myProject = project; myProcessor = fileProcessor; } @Override public void run() { while (true) { if (myProject.isDisposed() || myInnerIndicator.isCanceled()) { return; } try { final FileContent fileContent = myQueue.take(myInnerIndicator); if (fileContent == null) { myFinished.set(Boolean.TRUE); return; } final Runnable action = new Runnable() { @Override public void run() { myInnerIndicator.checkCanceled(); if (!myProject.isDisposed()) { final VirtualFile file = fileContent.getVirtualFile(); try { myProgressUpdater.consume(file); if (file.isValid() && !file.isDirectory() && !Boolean.TRUE.equals(file.getUserData(FAILED_TO_INDEX))) { myProcessor.consume(fileContent); } } catch (ProcessCanceledException e) { throw e; } catch (Throwable e) { LOG.error("Error while indexing " + file.getPresentableUrl() + "\n" + "To reindex this file IDEA has to be restarted", e); file.putUserData(FAILED_TO_INDEX, Boolean.TRUE); } } } }; try { ProgressManager.getInstance().runProcess( new Runnable() { @Override public void run() { if (myProcessInReadAction) { ApplicationManager.getApplication().runReadAction(action); } else { action.run(); } } }, ProgressWrapper.wrap(myInnerIndicator) ); } catch (ProcessCanceledException e) { myQueue.pushback(fileContent); return; } finally { myQueue.release(fileContent); } } catch (ProcessCanceledException e) { return; } } } } private static Runnable getProcessWrapper(final Runnable process) { // launching thread will hold read access for workers return ApplicationManager.getApplication().isReadAccessAllowed() ? new Runnable() { @Override public void run() { ApplicationImpl.setExceptionalThreadWithReadAccessFlag(true); try { process.run(); } finally { ApplicationImpl.setExceptionalThreadWithReadAccessFlag(false); } } } : process; } }