blob: 76377ef6b98effc746e511f0a58642df0cce1944 [file] [log] [blame]
/*
* 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<CommitTask> documentsToCommit = new Queue<CommitTask>(10);
private final List<CommitTask> documentsToApplyInEDT = new ArrayList<CommitTask>(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<CommitTask>() {
@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<Processor<Document>> finishProcessors = new SmartList<Processor<Document>>();
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<PsiFile> psiFiles = viewProvider.getAllFiles();
for (PsiFile file : psiFiles) {
if (file.isValid()) {
Processor<Document> 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<Document> handleCommitWithoutPsi(@NotNull final PsiDocumentManagerImpl documentManager,
@NotNull Document document,
@NotNull final CommitTask task,
final boolean synchronously) {
final long startDocModificationTimeStamp = document.getModificationStamp();
return new Processor<Document>() {
@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<CommitTask> processor) {
final boolean[] result = {true};
synchronized (documentsToCommit) {
documentsToCommit.process(new Processor<CommitTask>() {
@Override
public boolean process(CommitTask commitTask) {
result[0] &= processor.process(commitTask);
return true;
}
});
}
return result[0];
}
@TestOnly
boolean isEnabled() {
return myEnabled;
}
}