BlueMap/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/rendermanager/RenderManager.java

359 lines
12 KiB
Java

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
public class RenderManager {
private static final AtomicInteger nextRenderManagerIndex = new AtomicInteger(0);
@DebugDump private final int id;
@DebugDump private volatile boolean running;
@DebugDump private long lastTimeBusy;
private final AtomicInteger nextWorkerThreadIndex;
@DebugDump private final Collection<WorkerThread> workerThreads;
private final AtomicInteger busyCount;
private ProgressTracker progressTracker;
private volatile boolean newTask;
@DebugDump private final LinkedList<RenderTask> renderTasks;
public RenderManager() {
this.id = nextRenderManagerIndex.getAndIncrement();
this.nextWorkerThreadIndex = new AtomicInteger(0);
this.running = false;
this.workerThreads = new ConcurrentLinkedDeque<>();
this.busyCount = new AtomicInteger(0);
this.lastTimeBusy = -1;
this.progressTracker = null;
this.newTask = true;
this.renderTasks = new LinkedList<>();
}
public void start(int threadCount) throws IllegalStateException {
if (threadCount <= 0) throw new IllegalArgumentException("threadCount has to be 1 or more!");
synchronized (this.workerThreads) {
if (isRunning()) throw new IllegalStateException("RenderManager is already running!");
this.workerThreads.clear();
this.busyCount.set(0);
if (progressTracker != null) progressTracker.cancel();
progressTracker = new ProgressTracker(5000, 12); // 5-sec steps over one minute
this.newTask = true;
this.running = true;
for (int i = 0; i < threadCount; i++) {
WorkerThread worker = new WorkerThread();
this.workerThreads.add(worker);
worker.start();
}
}
}
public void stop() {
synchronized (this.workerThreads) {
this.running = false;
for (WorkerThread worker : workerThreads) worker.interrupt();
if (progressTracker != null) progressTracker.cancel();
}
}
public boolean isRunning() {
synchronized (this.workerThreads) {
for (WorkerThread worker : workerThreads) {
if (worker.isAlive()) return true;
}
return false;
}
}
public void awaitIdle() throws InterruptedException {
synchronized (this.renderTasks) {
while (!this.renderTasks.isEmpty())
this.renderTasks.wait(10000);
}
}
public void awaitShutdown() throws InterruptedException {
synchronized (this.workerThreads) {
while (isRunning())
this.workerThreads.wait(10000);
}
}
public boolean scheduleRenderTask(RenderTask task) {
synchronized (this.renderTasks) {
if (containsRenderTask(task)) return false;
removeTasksThatAreContainedIn(task);
renderTasks.addLast(task);
renderTasks.notifyAll();
return true;
}
}
public int scheduleRenderTasks(RenderTask... tasks) {
return scheduleRenderTasks(Arrays.asList(tasks));
}
public int scheduleRenderTasks(Collection<RenderTask> tasks) {
synchronized (this.renderTasks) {
int count = 0;
for (RenderTask task : tasks) {
if (scheduleRenderTask(task)) count++;
}
return count;
}
}
public boolean scheduleRenderTaskNext(RenderTask task) {
synchronized (this.renderTasks) {
if (renderTasks.size() <= 1) return scheduleRenderTask(task);
if (containsRenderTask(task)) return false;
removeTasksThatAreContainedIn(task);
renderTasks.add(1, task);
renderTasks.notifyAll();
return true;
}
}
public void reorderRenderTasks(Comparator<RenderTask> taskComparator) {
synchronized (this.renderTasks) {
if (renderTasks.size() <= 2) return;
RenderTask currentTask = renderTasks.removeFirst();
renderTasks.sort(taskComparator);
renderTasks.addFirst(currentTask);
}
}
public boolean removeRenderTask(RenderTask task) {
synchronized (this.renderTasks) {
if (this.renderTasks.isEmpty()) return false;
// cancel the task if it is currently processed
RenderTask first = renderTasks.getFirst();
if (first.equals(task)) {
first.cancel();
return true;
}
// else remove it
return renderTasks.remove(task);
}
}
public void removeRenderTasksIf(Predicate<RenderTask> removeCondition) {
synchronized (this.renderTasks) {
if (this.renderTasks.isEmpty()) return;
RenderTask first = renderTasks.removeFirst();
if (removeCondition.test(first)) first.cancel();
renderTasks.removeIf(removeCondition);
renderTasks.addFirst(first);
}
}
public void removeAllRenderTasks() {
synchronized (this.renderTasks) {
if (this.renderTasks.isEmpty()) return;
RenderTask first = renderTasks.removeFirst();
first.cancel();
renderTasks.clear();
renderTasks.addFirst(first);
}
}
public long estimateCurrentRenderTaskTimeRemaining() {
if (progressTracker == null) return 0;
synchronized (this.renderTasks) {
RenderTask task = getCurrentRenderTask();
if (task == null) return 0;
double progress = task.estimateProgress();
long timePerProgress = progressTracker.getAverageTimePerProgress();
return (long) ((1 - progress) * timePerProgress);
}
}
public RenderTask getCurrentRenderTask() {
synchronized (this.renderTasks) {
if (this.renderTasks.isEmpty()) return null;
return this.renderTasks.getFirst();
}
}
public List<RenderTask> getScheduledRenderTasks() {
synchronized (this.renderTasks) {
return new ArrayList<>(this.renderTasks);
}
}
public int getScheduledRenderTaskCount() {
return this.renderTasks.size();
}
public boolean containsRenderTask(RenderTask task) {
synchronized (this.renderTasks) {
// checking all scheduled renderTasks except the first one, since that is already being processed
Iterator<RenderTask> iterator = renderTasks.iterator();
if (!iterator.hasNext()) return false;
iterator.next(); // skip first
while(iterator.hasNext()) {
if (iterator.next().contains(task)) return true;
}
return false;
}
}
public int getWorkerThreadCount() {
return workerThreads.size();
}
public long getLastTimeBusy() {
return lastTimeBusy;
}
private void removeTasksThatAreContainedIn(RenderTask containingTask) {
synchronized (this.renderTasks) {
if (renderTasks.size() < 2) return;
RenderTask first = renderTasks.removeFirst();
if (containingTask.contains(first)) first.cancel();
renderTasks.removeIf(containingTask::contains);
renderTasks.addFirst(first);
}
}
private void doWork() throws Exception {
RenderTask task;
synchronized (this.renderTasks) {
while (this.renderTasks.isEmpty())
this.renderTasks.wait(10000);
task = this.renderTasks.getFirst();
if (this.newTask) {
this.newTask = false;
this.progressTracker.resetAndStart(task::estimateProgress);
}
// the following is making sure every render-thread is done working on this task (no thread is "busy")
// before continuing working on the next RenderTask
if (!task.hasMoreWork()) {
if (busyCount.get() <= 0) {
this.renderTasks.removeFirst();
this.renderTasks.notifyAll();
this.newTask = true;
busyCount.set(0);
} else {
this.renderTasks.wait(10000);
}
return;
}
this.busyCount.incrementAndGet();
this.lastTimeBusy = System.currentTimeMillis();
}
try {
task.doWork();
} finally {
synchronized (renderTasks) {
int busyCount = this.busyCount.decrementAndGet();
if (busyCount > 0) this.lastTimeBusy = System.currentTimeMillis();
this.renderTasks.notifyAll();
}
}
}
public class WorkerThread extends Thread {
private final int id;
private WorkerThread() {
this.id = RenderManager.this.nextWorkerThreadIndex.getAndIncrement();
this.setName("RenderManager-" + RenderManager.this.id + "-" + this.id);
}
@Override
@SuppressWarnings("BusyWait")
public void run() {
try {
while (RenderManager.this.running) {
try {
RenderManager.this.doWork();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
Logger.global.logError(
"RenderManager(" + RenderManager.this.id + "): WorkerThread(" + this.id +
"): Exception while doing some work!", e);
try {
// on error, wait a few seconds before resurrecting this render-thread
// if something goes wrong, this prevents running into the same error on all render-threads
// with full-speed over and over again :D
Thread.sleep(10000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
} finally {
synchronized (RenderManager.this.workerThreads) {
RenderManager.this.workerThreads.remove(this);
RenderManager.this.workerThreads.notifyAll();
}
}
}
}
}