Java tutorial
// 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 org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ProgressMonitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; /** * 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 Logger log = LoggerFactory.getLogger(MultiProgressMonitor.class); /** 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(final String subTaskName, final 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(final int completed) { boolean w = false; synchronized (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 synchronized int getCount() { 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(final OutputStream out, final 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(final OutputStream out, final 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(final 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(final Future<?> workerFuture, final long timeoutTime, final 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)); log.warn(String.format("MultiProgressMonitor worker killed after %sms" + detailMessage, // TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS))); } 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. log.warn("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(final String subTask, final 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.count; 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; } } } }