/* * 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.psi.impl; import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator; import com.intellij.ide.startup.impl.StartupManagerImpl; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationAdapter; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; 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.project.Project; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.util.Processor; import com.intellij.util.SmartList; import com.intellij.util.containers.Queue; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; public class DocumentCommitThread extends DocumentCommitProcessor implements Runnable, Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.DocumentCommitThread"); private final Queue documentsToCommit = new Queue(10); private final List documentsToApplyInEDT = new ArrayList(10); // guarded by documentsToCommit private final ApplicationEx myApplication; private volatile boolean isDisposed; private CommitTask currentTask; // guarded by documentsToCommit private volatile boolean threadFinished; private volatile boolean myEnabled; // true if we can do commits. set to false temporarily during the write action. public static DocumentCommitThread getInstance() { return ServiceManager.getService(DocumentCommitThread.class); } public DocumentCommitThread(final ApplicationEx application) { myApplication = application; // install listener in EDT to avoid missing events in case we are inside write action right now application.invokeLater(new Runnable() { @Override public void run() { application.addApplicationListener(new ApplicationAdapter() { private int runningWriteActions; @Override public void beforeWriteActionStart(Object action) { if (runningWriteActions++ == 0) { disable("Write action started: " + action); } } @Override public void writeActionFinished(Object action) { if (--runningWriteActions == 0) { enable("Write action finished: " + action); } } }, DocumentCommitThread.this); enable("Listener installed, started"); } }); log("Starting thread", null, false); new Thread(this, "Document commit thread").start(); } @Override public void dispose() { isDisposed = true; synchronized (documentsToCommit) { documentsToCommit.clear(); } cancel("Stop thread"); wakeUpQueue(); while (!threadFinished) { wakeUpQueue(); synchronized (documentsToCommit) { try { documentsToCommit.wait(10); } catch (InterruptedException ignored) { } } } } private void disable(@NonNls Object reason) { // write action has just started, all commits are useless cancel(reason); myEnabled = false; log("Disabled", null, false, reason); } private void enable(@NonNls Object reason) { myEnabled = true; wakeUpQueue(); log("Enabled", null, false, reason); } private void wakeUpQueue() { synchronized (documentsToCommit) { documentsToCommit.notifyAll(); } } private void cancel(@NonNls @NotNull Object reason) { startNewTask(null, reason); } @Override public void commitAsynchronously(@NotNull final Project project, @NotNull final Document document, @NonNls @NotNull Object reason) { queueCommit(project, document, reason); } public void queueCommit(@NotNull final Project project, @NotNull final Document document, @NonNls @NotNull Object reason) { assert !isDisposed : "already disposed"; if (!project.isInitialized()) return; PsiFile psiFile = PsiDocumentManager.getInstance(project).getCachedPsiFile(document); if (psiFile == null) return; doQueue(project, document, reason); } private void doQueue(@NotNull Project project, @NotNull Document document, @NotNull Object reason) { synchronized (documentsToCommit) { ProgressIndicator indicator = new DaemonProgressIndicator(); CommitTask newTask = new CommitTask(document, project, indicator, reason); markRemovedFromDocsToCommit(newTask); markRemovedCurrentTask(newTask); removeFromDocsToApplyInEDT(newTask); documentsToCommit.addLast(newTask); log("Queued", newTask, false, reason); wakeUpQueue(); } } private final StringBuilder log = new StringBuilder(); @Override public void log(@NonNls String msg, @Nullable CommitTask task, boolean synchronously, @NonNls Object... args) { if (true) return; String indent = new SimpleDateFormat("mm:ss:SSSS").format(new Date()) + (SwingUtilities.isEventDispatchThread() ? "- " : Thread.currentThread().getName().equals("Document commit thread") ? "- >" : "-"); @NonNls String s = indent + msg + (synchronously ? " (sync)" : "") + (task == null ? "" : "; task: " + task+" ("+System.identityHashCode(task)+")"); for (Object arg : args) { if (!StringUtil.isEmpty(String.valueOf(arg))) { s += "; "+arg; } } if (task != null) { boolean stillUncommitted = !task.project.isDisposed() && ((PsiDocumentManagerImpl)PsiDocumentManager.getInstance(task.project)).isInUncommittedSet(task.document); if (stillUncommitted) { s += "; Uncommitted: " + task.document; } } System.err.println(s); log.append(s).append("\n"); if (log.length() > 1000000) { log.delete(0, 1000000); } } // cancels all pending commits @TestOnly public void cancelAll() { synchronized (documentsToCommit) { cancel("cancel all in tests"); markRemovedFromDocsToCommit(null); documentsToCommit.clear(); removeFromDocsToApplyInEDT(null); markRemovedCurrentTask(null); } } @TestOnly public void clearQueue() { cancelAll(); log.setLength(0); wakeUpQueue(); } private void markRemovedCurrentTask(@Nullable CommitTask newTask) { CommitTask task = currentTask; if (task != null && (newTask == null || task.equals(newTask))) { task.removed = true; cancel("Sync commit intervened"); } } private void removeFromDocsToApplyInEDT(@Nullable("null means all") CommitTask newTask) { for (int i = documentsToApplyInEDT.size() - 1; i >= 0; i--) { CommitTask task = documentsToApplyInEDT.get(i); if (newTask == null || task.equals(newTask)) { task.removed = true; documentsToApplyInEDT.remove(i); log("Marked and Removed from EDT apply queue (sync commit called)", task, true); } } } private void markRemovedFromDocsToCommit(@Nullable("null means all") final CommitTask newTask) { processAll(new Processor() { @Override public boolean process(CommitTask task) { if (newTask == null || task.equals(newTask)) { task.removed = true; log("marker as Removed in background queue", task, true); } return true; } }); } @Override public void run() { threadFinished = false; try { while (!isDisposed) { try { pollQueue(); } catch(Throwable e) { LOG.error(e); } } } finally { threadFinished = true; } // ping the thread waiting for close wakeUpQueue(); log("Good bye", null, false); } private void pollQueue() { boolean success = false; Document document = null; Project project = null; CommitTask task = null; try { ProgressIndicator indicator; synchronized (documentsToCommit) { if (!myEnabled || documentsToCommit.isEmpty()) { documentsToCommit.wait(); return; } task = documentsToCommit.pullFirst(); document = task.document; indicator = task.indicator; project = task.project; log("Pulled", task, false, indicator); if (project.isDisposed() || !((PsiDocumentManagerImpl)PsiDocumentManager.getInstance(project)).isInUncommittedSet(document)) { log("Abandon and proceed to next",task, false); return; } if (task.removed) { return; // document has been marked as removed, e.g. by synchronous commit } startNewTask(task, "Pulled new task"); // transfer to documentsToApplyInEDT documentsToApplyInEDT.add(task); } Runnable finishRunnable = null; if (indicator.isCanceled()) { success = false; } else { final CommitTask commitTask = task; final Runnable[] result = new Runnable[1]; ProgressManager.getInstance().executeProcessUnderProgress(new Runnable() { @Override public void run() { result[0] = commitUnderProgress(commitTask, false); } }, commitTask.indicator); finishRunnable = result[0]; success = finishRunnable != null; log("commit returned", task, false, finishRunnable, indicator); } if (success) { assert !myApplication.isDispatchThread(); UIUtil.invokeLaterIfNeeded(finishRunnable); log("Invoked later finishRunnable", task, false, finishRunnable, indicator); } } catch (ProcessCanceledException e) { cancel(e); // leave queue unchanged log("PCE", task, false, e); success = false; } catch (InterruptedException e) { // app must be closing log("IE", task, false, e); cancel(e); } catch (Throwable e) { LOG.error(e); cancel(e); } synchronized (documentsToCommit) { if (!success && !task.removed) { // sync commit has not intervened // reset status for queue back successfully doQueue(project, document, "re-added on failure"); } currentTask = null; // do not cancel, it's being invokeLatered } } @Override public void commitSynchronously(@NotNull Document document, @NotNull Project project) { assert !isDisposed; myApplication.assertWriteAccessAllowed(); if (!project.isInitialized() && !project.isDefault()) { @NonNls String s = project + "; Disposed: "+project.isDisposed()+"; Open: "+project.isOpen(); s += "; SA Passed: "; try { s += ((StartupManagerImpl)StartupManager.getInstance(project)).startupActivityPassed(); } catch (Exception e) { s += e; } try { Disposer.dispose(project); } catch (Throwable ignored) { // do not fill log with endless exceptions } throw new RuntimeException(s); } ProgressIndicator indicator = createProgressIndicator(); CommitTask task = new CommitTask(document, project, indicator, "Sync commit"); synchronized (documentsToCommit) { markRemovedFromDocsToCommit(task); markRemovedCurrentTask(task); removeFromDocsToApplyInEDT(task); } log("About to commit sync", task, true, indicator); Runnable finish = commitUnderProgress(task, true); log("Committed sync", task, true, finish, indicator); assert finish != null; finish.run(); // let our thread know that queue must be polled again wakeUpQueue(); } @NotNull @Override protected ProgressIndicator createProgressIndicator() { return new ProgressIndicatorBase(); } private void startNewTask(@Nullable CommitTask task, @NotNull Object reason) { synchronized (documentsToCommit) { // sync to prevent overwriting CommitTask cur = currentTask; if (cur != null) { cur.indicator.cancel(); } currentTask = task; } log("new task started", task, false, reason); } // returns finish commit Runnable (to be invoked later in EDT), or null on failure @Nullable private Runnable commitUnderProgress(@NotNull final CommitTask task, final boolean synchronously) { final Project project = task.project; final Document document = task.document; final List> finishProcessors = new SmartList>(); Runnable runnable = new Runnable() { @Override public void run() { myApplication.assertReadAccessAllowed(); if (project.isDisposed()) return; final PsiDocumentManagerImpl documentManager = (PsiDocumentManagerImpl)PsiDocumentManager.getInstance(project); if (documentManager.isCommitted(document)) return; FileViewProvider viewProvider = documentManager.getCachedViewProvider(document); if (viewProvider == null) { finishProcessors.add(handleCommitWithoutPsi(documentManager, document, task, synchronously)); return; } List psiFiles = viewProvider.getAllFiles(); for (PsiFile file : psiFiles) { if (file.isValid()) { Processor finishProcessor = doCommit(task, file, synchronously); if (finishProcessor != null) { finishProcessors.add(finishProcessor); } } } } }; if (synchronously) { myApplication.assertWriteAccessAllowed(); runnable.run(); } else if (!myApplication.tryRunReadAction(runnable)) { log("Could not start read action", task, synchronously, myApplication.isReadAccessAllowed(), Thread.currentThread()); return null; } boolean canceled = task.indicator.isCanceled(); assert !synchronously || !canceled; if (canceled || task.removed) { return null; } Runnable finishRunnable = new Runnable() { @Override public void run() { myApplication.assertIsDispatchThread(); Project project = task.project; if (project.isDisposed()) return; Document document = task.document; synchronized (documentsToCommit) { boolean isValid = !task.removed; for (int i = documentsToApplyInEDT.size() - 1; i >= 0; i--) { CommitTask queuedTask = documentsToApplyInEDT.get(i); boolean taskIsValid = !queuedTask.removed; if (task == queuedTask) { // find the same task in the queue documentsToApplyInEDT.remove(i); isValid &= taskIsValid; log("Task matched, removed from documentsToApplyInEDT", queuedTask, false, task); } else if (!taskIsValid) { documentsToApplyInEDT.remove(i); log("Task invalid, removed from documentsToApplyInEDT", queuedTask, false); } } if (!isValid) { log("Marked as already committed in EDT apply queue, return", task, true); return; } } PsiDocumentManagerImpl documentManager = (PsiDocumentManagerImpl)PsiDocumentManager.getInstance(project); log("Executing later finishCommit", task, false); boolean success = documentManager.finishCommit(document, finishProcessors, synchronously, task.reason); if (synchronously) { assert success; } log("after call finishCommit",task, synchronously, success); if (synchronously || success) { assert !documentManager.isInUncommittedSet(document); } if (!success) { // add document back to the queue queueCommit(project, document, "Re-added back"); } } }; return finishRunnable; } @NotNull private Processor handleCommitWithoutPsi(@NotNull final PsiDocumentManagerImpl documentManager, @NotNull Document document, @NotNull final CommitTask task, final boolean synchronously) { final long startDocModificationTimeStamp = document.getModificationStamp(); return new Processor() { @Override public boolean process(Document document) { log("Finishing without PSI", task, synchronously, document.getModificationStamp(), startDocModificationTimeStamp); if (document.getModificationStamp() != startDocModificationTimeStamp || documentManager.getCachedViewProvider(document) != null) { return false; } documentManager.handleCommitWithoutPsi(document); return true; } }; } private boolean processAll(final Processor processor) { final boolean[] result = {true}; synchronized (documentsToCommit) { documentsToCommit.process(new Processor() { @Override public boolean process(CommitTask commitTask) { result[0] &= processor.process(commitTask); return true; } }); } return result[0]; } @TestOnly boolean isEnabled() { return myEnabled; } @TestOnly public void waitUntilAllCommitted(long timeout) throws InterruptedException { if (!myEnabled) { throw new IllegalStateException("DocumentCommitThread is disabled"); } int attempts = 0; int delay = 100; synchronized (documentsToCommit) { while(!documentsToCommit.isEmpty() || currentTask != null) { documentsToCommit.wait(delay); if (delay * attempts > timeout) { throw new RuntimeException("timeout"); } attempts++; } } } }