Implement a multi-sub-task progress monitor for ReceiveCommits
Because there are a lot of file and database accesses, having a progress monitor is useful. Moreover, we no longer rely on JGit for ref updates, so we can't depend on its progress meter. This is a special progress meter implementation that multiplexes output from the various sub-tasks that ReceiveCommits performs. We need this multiplexing because we don't know ahead of time how much of most kinds of work must be performed. For example, a single ref update can close an arbitrary number of changes. The progress meter includes a "spinner" animation that updates the output every 500ms regardless of activity. Additionally, the whole progress meter updates whenever one sub-task increases by 1%. Because of this, the MultiProgressMonitor must run in the main thread to avoid I/O stalls, delegating work to a background worker. Change-Id: If7f355830387a1a36a4e3b7ca01693239ce2d956
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
// 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 org.eclipse.jgit.lib.Constants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private final String name;
|
||||
private final int total;
|
||||
private volatile 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 the worker thread.
|
||||
*
|
||||
* @param completed number of work units completed.
|
||||
*/
|
||||
public void update(final int completed) {
|
||||
count += completed;
|
||||
if (total != UNKNOWN) {
|
||||
int percent = count * 100 / total;
|
||||
if (percent > lastPercent) {
|
||||
lastPercent = percent;
|
||||
wakeUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that this sub-task is finished.
|
||||
* <p>
|
||||
* Must be called from the worker thread.
|
||||
*/
|
||||
public void end() {
|
||||
if (total == UNKNOWN && count > 0) {
|
||||
wakeUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final OutputStream out;
|
||||
private final String taskName;
|
||||
private final List<Task> tasks = new CopyOnWriteArrayList<Task>();
|
||||
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}.
|
||||
* <p>
|
||||
* Must be called from the main thread, <em>not</em> the worker thread. Once
|
||||
* the worker thread calls {@link #end()}, the future has an additional
|
||||
* <code>maxInterval</code> to finish before it is forcefully cancelled and
|
||||
* {@link ExecutionException} is thrown.
|
||||
*
|
||||
* @param workerFuture a future that returns when the worker thread is
|
||||
* finished.
|
||||
*
|
||||
* @throws ExecutionException if this thread or the worker thread was
|
||||
* interrupted, the worker was cancelled, or the worker timed out.
|
||||
*/
|
||||
public void waitFor(Future<?> workerFuture) throws ExecutionException {
|
||||
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.
|
||||
left -= System.nanoTime() - 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(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 the worker thread.
|
||||
*/
|
||||
public synchronized void end() {
|
||||
done = true;
|
||||
wakeUp();
|
||||
}
|
||||
|
||||
private void sendDone() {
|
||||
spinnerState = NO_SPINNER;
|
||||
StringBuilder s = format();
|
||||
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(' ').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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user