diff options
Diffstat (limited to 'java/com/google/gerrit/server/git/MultiProgressMonitor.java')
-rw-r--r-- | java/com/google/gerrit/server/git/MultiProgressMonitor.java | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java new file mode 100644 index 0000000000..b72ea92c0c --- /dev/null +++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java @@ -0,0 +1,353 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// 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.google.gerrit.server.git; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import com.google.common.base.Strings; +import com.google.common.flogger.FluentLogger; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ProgressMonitor; + +/** + * Progress reporting interface that multiplexes multiple sub-tasks. + * + * <p>Output is of the format: + * + * <pre> + * Task: subA: 1, subB: 75% (3/4) (-)\r + * Task: subA: 2, subB: 75% (3/4), subC: 1 (\)\r + * Task: subA: 2, subB: 100% (4/4), subC: 1 (|)\r + * Task: subA: 4, subB: 100% (4/4), subC: 4, done \n + * </pre> + * + * <p>Callers should try to keep task and sub-task descriptions short, since the output should fit + * on one terminal line. (Note that git clients do not accept terminal control characters, so true + * multi-line progress messages would be impossible.) + */ +public class MultiProgressMonitor { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** Constant indicating the total work units cannot be predicted. */ + public static final int UNKNOWN = 0; + + private static final char[] SPINNER_STATES = new char[] {'-', '\\', '|', '/'}; + private static final char NO_SPINNER = ' '; + + /** Handle for a sub-task. */ + public class Task implements ProgressMonitor { + private final String name; + private final int total; + private int count; + private int lastPercent; + + Task(String subTaskName, int totalWork) { + this.name = subTaskName; + this.total = totalWork; + } + + /** + * Indicate that work has been completed on this sub-task. + * + * <p>Must be called from a worker thread. + * + * @param completed number of work units completed. + */ + @Override + public void update(int completed) { + boolean w = false; + synchronized (MultiProgressMonitor.this) { + count += completed; + if (total != UNKNOWN) { + int percent = count * 100 / total; + if (percent > lastPercent) { + lastPercent = percent; + w = true; + } + } + } + if (w) { + wakeUp(); + } + } + + /** + * Indicate that this sub-task is finished. + * + * <p>Must be called from a worker thread. + */ + public void end() { + if (total == UNKNOWN && getCount() > 0) { + wakeUp(); + } + } + + @Override + public void start(int totalTasks) {} + + @Override + public void beginTask(String title, int totalWork) {} + + @Override + public void endTask() {} + + @Override + public boolean isCancelled() { + return false; + } + + public int getCount() { + synchronized (MultiProgressMonitor.this) { + return count; + } + } + } + + private final OutputStream out; + private final String taskName; + private final List<Task> tasks = new CopyOnWriteArrayList<>(); + private int spinnerIndex; + private char spinnerState = NO_SPINNER; + private boolean done; + private boolean write = true; + + private final long maxIntervalNanos; + + /** + * Create a new progress monitor for multiple sub-tasks. + * + * @param out stream for writing progress messages. + * @param taskName name of the overall task. + */ + public MultiProgressMonitor(OutputStream out, String taskName) { + this(out, taskName, 500, TimeUnit.MILLISECONDS); + } + + /** + * Create a new progress monitor for multiple sub-tasks. + * + * @param out stream for writing progress messages. + * @param taskName name of the overall task. + * @param maxIntervalTime maximum interval between progress messages. + * @param maxIntervalUnit time unit for progress interval. + */ + public MultiProgressMonitor( + OutputStream out, String taskName, long maxIntervalTime, TimeUnit maxIntervalUnit) { + this.out = out; + this.taskName = taskName; + maxIntervalNanos = NANOSECONDS.convert(maxIntervalTime, maxIntervalUnit); + } + + /** + * Wait for a task managed by a {@link Future}, with no timeout. + * + * @see #waitFor(Future, long, TimeUnit) + */ + public void waitFor(Future<?> workerFuture) throws ExecutionException { + waitFor(workerFuture, 0, null); + } + + /** + * Wait for a task managed by a {@link Future}. + * + * <p>Must be called from the main thread, <em>not</em> a worker thread. Once a worker thread + * calls {@link #end()}, the future has an additional {@code maxInterval} to finish before it is + * forcefully cancelled and {@link ExecutionException} is thrown. + * + * @param workerFuture a future that returns when worker threads are finished. + * @param timeoutTime overall timeout for the task; the future is forcefully cancelled if the task + * exceeds the timeout. Non-positive values indicate no timeout. + * @param timeoutUnit unit for overall task timeout. + * @throws ExecutionException if this thread or a worker thread was interrupted, the worker was + * cancelled, or timed out waiting for a worker to call {@link #end()}. + */ + public void waitFor(Future<?> workerFuture, long timeoutTime, TimeUnit timeoutUnit) + throws ExecutionException { + long overallStart = System.nanoTime(); + long deadline; + String detailMessage = ""; + if (timeoutTime > 0) { + deadline = overallStart + NANOSECONDS.convert(timeoutTime, timeoutUnit); + } else { + deadline = 0; + } + + synchronized (this) { + long left = maxIntervalNanos; + while (!done) { + long start = System.nanoTime(); + try { + NANOSECONDS.timedWait(this, left); + } catch (InterruptedException e) { + throw new ExecutionException(e); + } + + // Send an update on every wakeup (manual or spurious), but only move + // the spinner every maxInterval. + long now = System.nanoTime(); + + if (deadline > 0 && now > deadline) { + workerFuture.cancel(true); + if (workerFuture.isCancelled()) { + detailMessage = + String.format( + "(timeout %sms, cancelled)", + TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS)); + logger.atWarning().log( + "MultiProgressMonitor worker killed after %sms: %s", + TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS), detailMessage); + } + break; + } + + left -= now - start; + if (left <= 0) { + moveSpinner(); + left = maxIntervalNanos; + } + sendUpdate(); + if (!done && workerFuture.isDone()) { + // The worker may not have called end() explicitly, which is likely a + // programming error. + logger.atWarning().log("MultiProgressMonitor worker did not call end() before returning"); + end(); + } + } + sendDone(); + } + + // The loop exits as soon as the worker calls end(), but we give it another + // maxInterval to finish up and return. + try { + workerFuture.get(maxIntervalNanos, NANOSECONDS); + } catch (InterruptedException e) { + throw new ExecutionException(e); + } catch (CancellationException e) { + throw new ExecutionException(detailMessage, e); + } catch (TimeoutException e) { + workerFuture.cancel(true); + throw new ExecutionException(e); + } + } + + private synchronized void wakeUp() { + notifyAll(); + } + + /** + * Begin a sub-task. + * + * @param subTask sub-task name. + * @param subTaskWork total work units in sub-task, or {@link #UNKNOWN}. + * @return sub-task handle. + */ + public Task beginSubTask(String subTask, int subTaskWork) { + Task task = new Task(subTask, subTaskWork); + tasks.add(task); + return task; + } + + /** + * End the overall task. + * + * <p>Must be called from a worker thread. + */ + public synchronized void end() { + done = true; + wakeUp(); + } + + private void sendDone() { + spinnerState = NO_SPINNER; + StringBuilder s = format(); + boolean any = false; + for (Task t : tasks) { + if (t.count != 0) { + any = true; + break; + } + } + if (any) { + s.append(","); + } + s.append(" done \n"); + send(s); + } + + private void moveSpinner() { + spinnerIndex = (spinnerIndex + 1) % SPINNER_STATES.length; + spinnerState = SPINNER_STATES[spinnerIndex]; + } + + private void sendUpdate() { + send(format()); + } + + private StringBuilder format() { + StringBuilder s = new StringBuilder().append("\r").append(taskName).append(':'); + + if (!tasks.isEmpty()) { + boolean first = true; + for (Task t : tasks) { + int count = t.getCount(); + if (count == 0) { + continue; + } + + if (!first) { + s.append(','); + } else { + first = false; + } + + s.append(' '); + if (!Strings.isNullOrEmpty(t.name)) { + s.append(t.name).append(": "); + } + if (t.total == UNKNOWN) { + s.append(count); + } else { + s.append(String.format("%d%% (%d/%d)", count * 100 / t.total, count, t.total)); + } + } + } + + if (spinnerState != NO_SPINNER) { + // Don't output a spinner until the alarm fires for the first time. + s.append(" (").append(spinnerState).append(')'); + } + return s; + } + + private void send(StringBuilder s) { + if (write) { + try { + out.write(Constants.encode(s.toString())); + out.flush(); + } catch (IOException e) { + write = false; + } + } + } +} |