From 68ab451d3907e38f7c259ef52a9c3c6a6ee42ead Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Mon, 28 Feb 2011 19:34:06 -0800 Subject: [PATCH] ProgressMonitor: Refactor to use background alarms Instead of polling the system clock on every update(1) method call, use a scheduled executor to toggle a volatile once per second until the task is done. Check the volatile on each update(int), looking to see if output should occur. This limits progress output to either once per 1% complete, or once per second. To save time during update calls the timer isn't reset during each 1% of output, which means we may see one unnecessary output trigger if at least 1% completed during the one second of the alarm time. Change-Id: I8fdd7e31c37bef39a5d1b3da7105da0ef879eb84 Signed-off-by: Shawn O. Pearce --- .../src/org/eclipse/jgit/pgm/Diff.java | 5 +- .../jgit/lib/BatchingProgressMonitor.java | 271 ++++++++++++++++++ .../eclipse/jgit/lib/TextProgressMonitor.java | 152 +++++----- .../transport/SideBandProgressMonitor.java | 141 ++++----- 4 files changed, 411 insertions(+), 158 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java index 506031713..5cc058950 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java @@ -52,6 +52,7 @@ import java.io.BufferedOutputStream; import java.io.PrintWriter; import java.text.MessageFormat; import java.util.List; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; @@ -189,7 +190,9 @@ class Diff extends TextBuiltin { } else if (newTree == null) newTree = new FileTreeIterator(db); - diffFmt.setProgressMonitor(new TextProgressMonitor()); + TextProgressMonitor pm = new TextProgressMonitor(); + pm.setDelayStart(2, TimeUnit.SECONDS); + diffFmt.setProgressMonitor(pm); diffFmt.setPathFilter(pathFilter); if (detectRenames != null) diffFmt.setDetectRenames(detectRenames.booleanValue()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java new file mode 100644 index 000000000..5eb959752 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2008-2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** ProgressMonitor that batches update events. */ +public abstract class BatchingProgressMonitor implements ProgressMonitor { + private static final ScheduledThreadPoolExecutor alarmQueue; + + static final Object alarmQueueKiller; + + static { + // To support garbage collection, start our thread but + // swap out the thread factory. When our class is GC'd + // the alarmQueueKiller will finalize and ask the executor + // to shutdown, ending the worker. + // + int threads = 1; + alarmQueue = new ScheduledThreadPoolExecutor(threads, + new ThreadFactory() { + public Thread newThread(Runnable taskBody) { + Thread thr = new Thread("JGit-AlarmQueue"); + thr.setDaemon(true); + return thr; + } + }); + alarmQueue.allowCoreThreadTimeOut(false); + alarmQueue.setMaximumPoolSize(alarmQueue.getCorePoolSize()); + alarmQueue.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + alarmQueue.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + alarmQueue.prestartAllCoreThreads(); + + // Now that the threads are running, its critical to swap out + // our own thread factory for one that isn't in the ClassLoader. + // This allows the class to GC. + // + alarmQueue.setThreadFactory(Executors.defaultThreadFactory()); + + alarmQueueKiller = new Object() { + @Override + protected void finalize() { + alarmQueue.shutdownNow(); + } + }; + } + + private long delayStartTime; + + private TimeUnit delayStartUnit = TimeUnit.MILLISECONDS; + + private Task task; + + /** + * Set an optional delay before the first output. + * + * @param time + * how long to wait before output. If 0 output begins on the + * first {@link #update(int)} call. + * @param unit + * time unit of {@code time}. + */ + public void setDelayStart(long time, TimeUnit unit) { + delayStartTime = time; + delayStartUnit = unit; + } + + public void start(int totalTasks) { + // Ignore the number of tasks. + } + + public void beginTask(String title, int work) { + endTask(); + task = new Task(title, work); + if (delayStartTime != 0) + task.delay(delayStartTime, delayStartUnit); + } + + public void update(int completed) { + if (task != null) + task.update(this, completed); + } + + public void endTask() { + if (task != null) { + task.end(this); + task = null; + } + } + + public boolean isCancelled() { + return false; + } + + /** + * Update the progress monitor if the total work isn't known, + * + * @param taskName + * name of the task. + * @param workCurr + * number of units already completed. + */ + protected abstract void onUpdate(String taskName, int workCurr); + + /** + * Finish the progress monitor when the total wasn't known in advance. + * + * @param taskName + * name of the task. + * @param workCurr + * total number of units processed. + */ + protected abstract void onEndTask(String taskName, int workCurr); + + /** + * Update the progress monitor when the total is known in advance. + * + * @param taskName + * name of the task. + * @param workCurr + * number of units already completed. + * @param workTotal + * estimated number of units to process. + * @param percentDone + * {@code workCurr * 100 / workTotal}. + */ + protected abstract void onUpdate(String taskName, int workCurr, + int workTotal, int percentDone); + + /** + * Finish the progress monitor when the total is known in advance. + * + * @param taskName + * name of the task. + * @param workCurr + * total number of units processed. + * @param workTotal + * estimated number of units to process. + * @param percentDone + * {@code workCurr * 100 / workTotal}. + */ + protected abstract void onEndTask(String taskName, int workCurr, + int workTotal, int percentDone); + + private static class Task implements Runnable { + /** Title of the current task. */ + private final String taskName; + + /** Number of work units, or {@link ProgressMonitor#UNKNOWN}. */ + private final int totalWork; + + /** True when timer expires and output should occur on next update. */ + private volatile boolean display; + + /** Scheduled timer, supporting cancellation if task ends early. */ + private Future timerFuture; + + /** True if the task has displayed anything. */ + private boolean output; + + /** Number of work units already completed. */ + private int lastWork; + + /** Percentage of {@link #totalWork} that is done. */ + private int lastPercent; + + Task(String taskName, int totalWork) { + this.taskName = taskName; + this.totalWork = totalWork; + this.display = true; + } + + void delay(long time, TimeUnit unit) { + display = false; + timerFuture = alarmQueue.schedule(this, time, unit); + } + + public void run() { + display = true; + } + + void update(BatchingProgressMonitor pm, int completed) { + lastWork += completed; + + if (totalWork == UNKNOWN) { + // Only display once per second, as the alarm fires. + if (display) { + pm.onUpdate(taskName, lastWork); + output = true; + restartTimer(); + } + } else { + // Display once per second or when 1% is done. + int currPercent = lastWork * 100 / totalWork; + if (display) { + pm.onUpdate(taskName, lastWork, totalWork, currPercent); + output = true; + restartTimer(); + lastPercent = currPercent; + } else if (currPercent != lastPercent) { + pm.onUpdate(taskName, lastWork, totalWork, currPercent); + output = true; + lastPercent = currPercent; + } + } + } + + private void restartTimer() { + display = false; + timerFuture = alarmQueue.schedule(this, 1, TimeUnit.SECONDS); + } + + void end(BatchingProgressMonitor pm) { + if (output) { + if (totalWork == UNKNOWN) { + pm.onEndTask(taskName, lastWork); + } else { + int pDone = lastWork * 100 / totalWork; + pm.onEndTask(taskName, lastWork, totalWork, pDone); + } + } + if (timerFuture != null) + timerFuture.cancel(false /* no interrupt */); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java index a668b11be..7e4860907 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java @@ -44,99 +44,103 @@ package org.eclipse.jgit.lib; -/** - * A simple progress reporter printing on stderr - */ -public class TextProgressMonitor implements ProgressMonitor { - private boolean output; - - private long taskBeganAt; - - private String msg; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; - private int lastWorked; +/** A simple progress reporter printing on a stream. */ +public class TextProgressMonitor extends BatchingProgressMonitor { + private final Writer out; - private int totalWork; + private boolean write; /** Initialize a new progress monitor. */ public TextProgressMonitor() { - taskBeganAt = System.currentTimeMillis(); + this(new PrintWriter(System.err)); } - public void start(final int totalTasks) { - // Ignore the number of tasks. - taskBeganAt = System.currentTimeMillis(); + /** + * Initialize a new progress monitor. + * + * @param out + * the stream to receive messages on. + */ + public TextProgressMonitor(Writer out) { + this.out = out; + this.write = true; } - public void beginTask(final String title, final int total) { - endTask(); - msg = title; - lastWorked = 0; - totalWork = total; + @Override + protected void onUpdate(String taskName, int workCurr) { + StringBuilder s = new StringBuilder(); + format(s, taskName, workCurr); + send(s); } - public void update(final int completed) { - if (msg == null) - return; - - final int cmp = lastWorked + completed; - if (!output && System.currentTimeMillis() - taskBeganAt < 500) - return; - if (totalWork == UNKNOWN) { - display(cmp); - System.err.flush(); - } else { - if ((cmp * 100 / totalWork) != (lastWorked * 100) / totalWork) { - display(cmp); - System.err.flush(); - } - } - lastWorked = cmp; - output = true; + @Override + protected void onEndTask(String taskName, int workCurr) { + StringBuilder s = new StringBuilder(); + format(s, taskName, workCurr); + s.append("\n"); + send(s); } - private void display(final int cmp) { - final StringBuilder m = new StringBuilder(); - m.append('\r'); - m.append(msg); - m.append(": "); - while (m.length() < 25) - m.append(' '); + private void format(StringBuilder s, String taskName, int workCurr) { + s.append("\r"); + s.append(taskName); + s.append(": "); + while (s.length() < 25) + s.append(' '); + s.append(workCurr); + } - if (totalWork == UNKNOWN) { - m.append(cmp); - } else { - final String twstr = String.valueOf(totalWork); - String cmpstr = String.valueOf(cmp); - while (cmpstr.length() < twstr.length()) - cmpstr = " " + cmpstr; - final int pcnt = (cmp * 100 / totalWork); - if (pcnt < 100) - m.append(' '); - if (pcnt < 10) - m.append(' '); - m.append(pcnt); - m.append("% ("); - m.append(cmpstr); - m.append("/"); - m.append(twstr); - m.append(")"); - } + @Override + protected void onUpdate(String taskName, int cmp, int totalWork, int pcnt) { + StringBuilder s = new StringBuilder(); + format(s, taskName, cmp, totalWork, pcnt); + send(s); + } - System.err.print(m); + @Override + protected void onEndTask(String taskName, int cmp, int totalWork, int pcnt) { + StringBuilder s = new StringBuilder(); + format(s, taskName, cmp, totalWork, pcnt); + s.append("\n"); + send(s); } - public boolean isCancelled() { - return false; + private void format(StringBuilder s, String taskName, int cmp, + int totalWork, int pcnt) { + s.append("\r"); + s.append(taskName); + s.append(": "); + while (s.length() < 25) + s.append(' '); + + String endStr = String.valueOf(totalWork); + String curStr = String.valueOf(cmp); + while (curStr.length() < endStr.length()) + curStr = " " + curStr; + if (pcnt < 100) + s.append(' '); + if (pcnt < 10) + s.append(' '); + s.append(pcnt); + s.append("% ("); + s.append(curStr); + s.append("/"); + s.append(endStr); + s.append(")"); } - public void endTask() { - if (output) { - if (totalWork != UNKNOWN) - display(totalWork); - System.err.println(); + private void send(StringBuilder s) { + if (write) { + try { + out.write(s.toString()); + out.flush(); + } catch (IOException err) { + write = false; + } } - output = false; - msg = null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java index efce7b1da..c7b22bdab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java @@ -43,110 +43,85 @@ package org.eclipse.jgit.transport; +import java.io.IOException; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; +import org.eclipse.jgit.lib.BatchingProgressMonitor; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.ProgressMonitor; /** Write progress messages out to the sideband channel. */ -class SideBandProgressMonitor implements ProgressMonitor { - private PrintWriter out; +class SideBandProgressMonitor extends BatchingProgressMonitor { + private final OutputStream out; - private boolean output; - - private long taskBeganAt; - - private long lastOutput; - - private String msg; - - private int lastWorked; - - private int totalWork; + private boolean write; SideBandProgressMonitor(final OutputStream os) { - out = new PrintWriter(new OutputStreamWriter(os, Constants.CHARSET)); + out = os; + write = true; } - public void start(final int totalTasks) { - // Ignore the number of tasks. - taskBeganAt = System.currentTimeMillis(); - lastOutput = taskBeganAt; + @Override + protected void onUpdate(String taskName, int workCurr) { + StringBuilder s = new StringBuilder(); + format(s, taskName, workCurr); + s.append(" \r"); + send(s); } - public void beginTask(final String title, final int total) { - endTask(); - msg = title; - lastWorked = 0; - totalWork = total; + @Override + protected void onEndTask(String taskName, int workCurr) { + StringBuilder s = new StringBuilder(); + format(s, taskName, workCurr); + s.append(", done\n"); + send(s); } - public void update(final int completed) { - if (msg == null) - return; - - final int cmp = lastWorked + completed; - final long now = System.currentTimeMillis(); - if (!output && now - taskBeganAt < 500) - return; - if (totalWork == UNKNOWN) { - if (now - lastOutput >= 500) { - display(cmp, null); - lastOutput = now; - } - } else { - if ((cmp * 100 / totalWork) != (lastWorked * 100) / totalWork - || now - lastOutput >= 500) { - display(cmp, null); - lastOutput = now; - } - } - lastWorked = cmp; - output = true; + private void format(StringBuilder s, String taskName, int workCurr) { + s.append(taskName); + s.append(": "); + s.append(workCurr); } - private void display(final int cmp, final String eol) { - final StringBuilder m = new StringBuilder(); - m.append(msg); - m.append(": "); + @Override + protected void onUpdate(String taskName, int cmp, int totalWork, int pcnt) { + StringBuilder s = new StringBuilder(); + format(s, taskName, cmp, totalWork, pcnt); + s.append(" \r"); + send(s); + } - if (totalWork == UNKNOWN) { - m.append(cmp); - } else { - final int pcnt = (cmp * 100 / totalWork); - if (pcnt < 100) - m.append(' '); - if (pcnt < 10) - m.append(' '); - m.append(pcnt); - m.append("% ("); - m.append(cmp); - m.append("/"); - m.append(totalWork); - m.append(")"); - } - if (eol != null) - m.append(eol); - else - m.append(" \r"); - out.print(m); - out.flush(); + @Override + protected void onEndTask(String taskName, int cmp, int totalWork, int pcnt) { + StringBuilder s = new StringBuilder(); + format(s, taskName, cmp, totalWork, pcnt); + s.append("\n"); + send(s); } - public boolean isCancelled() { - return false; + private void format(StringBuilder s, String taskName, int cmp, + int totalWork, int pcnt) { + s.append(taskName); + s.append(": "); + if (pcnt < 100) + s.append(' '); + if (pcnt < 10) + s.append(' '); + s.append(pcnt); + s.append("% ("); + s.append(cmp); + s.append("/"); + s.append(totalWork); + s.append(")"); } - public void endTask() { - if (output) { - if (totalWork == UNKNOWN) - display(lastWorked, ", done\n"); - else - display(totalWork, "\n"); + private void send(StringBuilder s) { + if (write) { + try { + out.write(Constants.encode(s.toString())); + out.flush(); + } catch (IOException err) { + write = false; + } } - output = false; - msg = null; } }