package org.plantuml.idea.rendering;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.plantuml.idea.toolwindow.ExecutionStatusPanel;
import java.util.concurrent.Future;
/**
* This Executor executes Runnables sequentially and is so lazy that it executes only last RenderCommand submitted while
* previously scheduled RenderCommand is running. Useful when you want to submit a lot of cumulative Runnables without
* performance impact.
*
* @author Eugene Steinberg
* @author Vojtech Krasa
*/
public class LazyApplicationPoolExecutor {
public static final Logger logger = Logger.getInstance(LazyApplicationPoolExecutor.class);
protected static final int MILLION = 1000000;
private final ExecutionStatusPanel executionStatusPanel;
protected RenderCommand nextCommand;
protected long startAfterNanos;
protected Future<?> future;
protected long delayNanos; // delay between command executions
protected final Object POOL_THREAD_STICK = new Object();
public LazyApplicationPoolExecutor(int delayMillis, @NotNull ExecutionStatusPanel executionStatusPanel) {
this.executionStatusPanel = executionStatusPanel;
setDelay(delayMillis);
startAfterNanos = 0;
}
public void setDelay(long delayMillis) {
this.delayNanos = delayMillis * MILLION;
logger.debug("settings delayNanos=", delayNanos);
setStartAfter();
synchronized (POOL_THREAD_STICK) {
POOL_THREAD_STICK.notifyAll();
}
}
/**
* Lazily executes the RenderCommand. Command will be queued for execution, but can be swallowed by another command
* if it will be submitted before this command will be scheduled for execution
*
* @param command command to be executed.
*/
public synchronized void execute(@NotNull final RenderCommand command) {
Delay delay = command.delay;
logger.debug("#execute ", command, " delay=", delay);
nextCommand = command;
if (delay == Delay.RESET_PRE_DELAY) {
setStartAfter();
} else if (delay == Delay.NOW) {
startAfterNanos = 0;
synchronized (POOL_THREAD_STICK) {
POOL_THREAD_STICK.notifyAll();
}
}
if (future == null || future.isDone()) {
scheduleNext(null);
} else if (command.reason == RenderCommand.Reason.REFRESH) {
future.cancel(true);
}
}
private synchronized void setStartAfter() {
startAfterNanos = System.nanoTime() + this.delayNanos;
}
private synchronized long getRemainingDelayMillis() {
return (startAfterNanos - System.nanoTime()) / MILLION;
}
private synchronized RenderCommand pollCommand() {
RenderCommand next = LazyApplicationPoolExecutor.this.nextCommand;
LazyApplicationPoolExecutor.this.nextCommand = null;
Thread.interrupted(); //clear flag
return next;
}
private synchronized void scheduleNext(final RenderCommand previousCommand) {
logger.debug("scheduleNext");
if (previousCommand != null && nextCommand != null && nextCommand.reason != RenderCommand.Reason.INCLUDES && nextCommand.reason != RenderCommand.Reason.REFRESH) {
if (previousCommand.page == nextCommand.page
&& previousCommand.zoom == nextCommand.zoom
&& previousCommand.sourceFilePath.equals(nextCommand.sourceFilePath)
&& previousCommand.source.equals(nextCommand.source)) {
logger.debug("nextCommand is same as previous, skipping");
nextCommand = null;
}
}
if (nextCommand != null) {
future = ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
executionStatusPanel.update(ExecutionStatusPanel.State.WAITING);
RenderCommand polledCommand = null;
try {
long delayRemaining = getRemainingDelayMillis();
while (delayRemaining - 5 > 0) {//tolerance
logger.debug("waiting ", delayRemaining, "ms");
synchronized (POOL_THREAD_STICK) {
POOL_THREAD_STICK.wait(delayRemaining);
}
delayRemaining = getRemainingDelayMillis();
}
polledCommand = pollCommand();
if (polledCommand != null) {
logger.debug("running command ", polledCommand);
long start = System.currentTimeMillis();
polledCommand.run();
logger.debug("command executed in ", System.currentTimeMillis() - start, "ms");
setStartAfter();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
scheduleNext(polledCommand); //needed to execute the very last command
}
}
});
}
}
public synchronized void cancel() {
nextCommand = null;
future.cancel(true);
}
public enum Delay {
RESET_PRE_DELAY,
NOW,
POST_DELAY;
}
}