package net.slightlymagic.braids.util.progress_monitor;
import java.util.Date;
import com.esotericsoftware.minlog.Log;
/**
* This base class also acts as a "null" progress monitor; it doesn't display
* anything when updated.
*
* Absolute times are measured in seconds, in congruence with ProgressMonitor.
*
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor
*/
public class BaseProgressMonitor implements BraidsProgressMonitor {
private int numPhases;
private int currentPhase;
private long totalUnitsThisPhase;
private long unitsCompletedSoFarThisPhase;
private float minUIUpdateIntervalSec;
private long lastUIUpdateTime;
private long phaseOneStartTime;
private long currentPhaseStartTime;
private float currentPhaseExponent;
private long[] phaseDurationHistorySecList;
private float[] phaseWeights;
public final int SECONDS_PER_MINUTE = 60;
public final int SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
public final int SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
/**
* Convenience for
* BaseProgressMonitor(numPhases, totalUnitsFirstPhase, 2.0f, null).
*
* @see #BaseProgressMonitor(int,long,float,float[])
*/
public BaseProgressMonitor(int numPhases, long totalUnitsFirstPhase) {
this(numPhases, totalUnitsFirstPhase, 2.0f, null);
}
/**
* Convenience for
* BaseProgressMonitor(numPhases, totalUnitsFirstPhase,
* minUIUpdateIntervalSec, null).
*
* @see #BaseProgressMonitor(int,long,float,float[])
*/
public BaseProgressMonitor(int numPhases, long totalUnitsFirstPhase,
float minUIUpdateIntervalSec)
{
this(numPhases, totalUnitsFirstPhase, minUIUpdateIntervalSec, null);
}
/**
* Initializes fields and starts the timers.
*
* @param numPhases the total number of phases we will monitor
*
* @param totalUnitsFirstPhase how many units to expect in phase 1
*
* @param minUIUpdateIntervalSec the approximate interval at which we
* update the user interface, in seconds
*
* @param phaseWeights may be null; if not null, this indicates the
* relative weight of each phase in terms of time to complete all phases.
* Index 0 of this array indicates phase 1's weight, index 1 indicates
* the weight of phase 2, and so on. If null, all phases are considered to
* take an equal amount of time to complete, which is equivalent to setting
* all phase weights to 1.0f. For example, if there are two phases, and
* the phase weights are set to {2.0f, 1.0f}, then the methods that compute
* the final ETA (Estimated Time of Arrival or completion) will assume that
* phase 2 takes half as long as phase 1. In other words, the operation
* will spend 67% of its time in phase 1, and 33% of its time in phase 2.
*/
public BaseProgressMonitor(int numPhases, long totalUnitsFirstPhase,
float minUIUpdateIntervalSec, float[] phaseWeights)
{
this.numPhases = numPhases;
this.currentPhase = 1;
this.unitsCompletedSoFarThisPhase = 0L;
this.minUIUpdateIntervalSec = minUIUpdateIntervalSec;
this.lastUIUpdateTime = 0L;
this.phaseOneStartTime = new Date().getTime()/1000;
this.currentPhaseStartTime = this.phaseOneStartTime;
this.currentPhaseExponent = 1;
this.phaseDurationHistorySecList = new long[numPhases];
if (phaseWeights == null) {
this.phaseWeights = new float[numPhases];
for (int ix = 0; ix < numPhases; ix++) {
this.phaseWeights[ix] = 1.0f;
}
}
else {
this.phaseWeights = phaseWeights;
}
setTotalUnitsThisPhase(totalUnitsFirstPhase);
}
/**
* Does nothing.
*/
public void dispose() {
;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getNumPhases()
*/
public int getNumPhases() {
return this.numPhases;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getMinUpdateIntervalSec()
*/
public float getMinUpdateIntervalSec() {
return this.minUIUpdateIntervalSec;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getCurrentPhase()
*/
public int getCurrentPhase() {
return this.currentPhase;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getUnitsCompletedSoFarThisPhase()
*/
public long getUnitsCompletedSoFarThisPhase() {
return this.unitsCompletedSoFarThisPhase;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getTotalUnitsThisPhase()
*/
public long getTotalUnitsThisPhase() {
return this.totalUnitsThisPhase;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getLastUIUpdateTime()
*/
public long getLastUIUpdateTime() {
return this.lastUIUpdateTime;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getPhaseOneStartTime()
*/
public long getPhaseOneStartTime() {
return this.phaseOneStartTime;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getCurrentPhaseStartTime()
*/
public long getCurrentPhaseStartTime() {
return this.currentPhaseStartTime;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#setMinUpdateIntervalSec(float)
*/
public void setMinUpdateIntervalSec(float value) {
this.minUIUpdateIntervalSec = value;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#setTotalUnitsThisPhase(long)
*/
public void setTotalUnitsThisPhase(long value) {
this.totalUnitsThisPhase = value;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getPercentCompleteOfThisPhaseAsString()
*/
public String getPercentCompleteOfThisPhaseAsString() {
Float percent = getPercentCompleteOfThisPhaseAsFloat();
if (percent != null) {
return Integer.toString((int) (float) percent);
}
else {
return "??";
}
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getTotalPercentCompleteAsString()
*/
public String getTotalPercentCompleteAsString() {
Float percent = getTotalPercentCompleteAsFloat();
if (percent == null) {
return "??";
}
else {
return Integer.toString((int) (float) percent);
}
}
/**
* Convenience for getRelativeETAAsString(false), meaning to compute the
* value for the end of the last phase.
*
* @see #getRelativeETAAsString(boolean)
*/
public String getRelativeETAAsString() {
return getRelativeETAAsString(false);
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getRelativeETAAsString(boolean)
*/
public String getRelativeETAAsString(boolean thisPhaseOnly) {
Integer etaSec = getRelativeETASec(thisPhaseOnly);
if (etaSec == null) {
return "unknown";
}
String result = "";
if (etaSec > SECONDS_PER_DAY) {
result += Integer.toString(etaSec / SECONDS_PER_DAY);
result += " da, ";
etaSec %= SECONDS_PER_DAY; // Shave off the portion recorded.
}
if (result.length() > 0 || etaSec > SECONDS_PER_HOUR) {
result += Integer.toString(etaSec / SECONDS_PER_HOUR);
result += " hr, ";
etaSec %= SECONDS_PER_HOUR; // Shave off the portion recorded.
}
if (result.length() > 0 || etaSec > SECONDS_PER_MINUTE) {
result += Integer.toString(etaSec / SECONDS_PER_MINUTE);
result += " min, ";
etaSec %= SECONDS_PER_MINUTE; // Shave off the portion recorded.
}
result += Integer.toString(etaSec);
result += " sec";
return result;
}
/**
* Convenience for getAbsoluteETAAsLocalTimeString(false), meaning to
* compute the value for the end of the last phase.
*
* @see #getAbsoluteETAAsLocalTimeString(boolean)
*/
public String getAbsoluteETAAsLocalTimeString() {
return getAbsoluteETAAsLocalTimeString(false);
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getAbsoluteETAAsLocalTimeString(boolean)
*/
public String getAbsoluteETAAsLocalTimeString(boolean thisPhaseOnly) {
Long etaTime = getAbsoluteETATime(thisPhaseOnly);
if (etaTime == null) {
return "unknown";
}
return (new Date(etaTime*1000).toString());
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#incrementUnitsCompletedThisPhase(long)
*/
public void incrementUnitsCompletedThisPhase(long numUnits) {
this.unitsCompletedSoFarThisPhase += numUnits;
}
/**
* Subclasses must call this immediately after updating the UI, to
* preserve the integrity of the shouldUpdateUI method.
*/
protected void justUpdatedUI() {
this.lastUIUpdateTime = new Date().getTime()/1000;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#shouldUpdateUI()
*/
public boolean shouldUpdateUI() {
doctorStartTimes();
long nowTime = (new Date().getTime()/1000);
if (nowTime - this.lastUIUpdateTime >= this.minUIUpdateIntervalSec ||
(this.getUnitsCompletedSoFarThisPhase() ==
this.getTotalUnitsThisPhase()))
{
return true;
}
else {
return false;
}
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#markCurrentPhaseAsComplete(long)
*/
public void markCurrentPhaseAsComplete(long totalUnitsNextPhase) {
if ((this.currentPhase > this.numPhases)) {
String message = "The phase just completed (";
message += this.currentPhase;
message += ") is greater than the total number ";
message += "of anticipated phases (";
message += this.numPhases;
message += "); the latter is probably incorrect.";
Log.warn(message);
}
this.currentPhase += 1;
this.unitsCompletedSoFarThisPhase = 0;
setTotalUnitsThisPhase(totalUnitsNextPhase);
this.currentPhaseExponent = 1;
long nowTime = (new Date().getTime()/1000);
long durationOfThisPhaseSec = nowTime - this.currentPhaseStartTime;
if (durationOfThisPhaseSec < 0) {
durationOfThisPhaseSec = 0;
}
if (0 <= currentPhase-2 && currentPhase-2 < phaseDurationHistorySecList.length) {
this.phaseDurationHistorySecList[currentPhase-2] = durationOfThisPhaseSec;
}
this.currentPhaseStartTime = nowTime;
if (this.currentPhase >= this.numPhases) {
String message = "Actual individual phase durations: [";
for (int ix = 0 ; ix < phaseDurationHistorySecList.length ; ix++) {
message += phaseDurationHistorySecList[ix] + ", ";
}
Log.info(message + ']');
}
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#sendMessage(java.lang.String)
*/
public void sendMessage(String message) {
;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#setCurrentPhaseAsExponential(float)
*/
public void setCurrentPhaseAsExponential(float value) {
this.currentPhaseExponent = value;
}
/**
* @see net.slightlymagic.braids.util.progress_monitor.BraidsProgressMonitor#getCurrentPhaseExponent()
*/
public float getCurrentPhaseExponent() {
return this.currentPhaseExponent;
}
/**
* @return number in range [0.0, 100.0] or null.
*/
protected Float getPercentCompleteOfThisPhaseAsFloat() {
if (this.totalUnitsThisPhase < 1 ||
this.unitsCompletedSoFarThisPhase > this.totalUnitsThisPhase) {
return null;
}
else {
float ratio = ((float) (this.unitsCompletedSoFarThisPhase)) /
((float) this.totalUnitsThisPhase);
ratio = (float) Math.pow(ratio, this.getCurrentPhaseExponent());
return (ratio * 100.0f);
}
}
/**
* Returns number in range [0.0, 100.0] or null.
*/
protected Float getTotalPercentCompleteAsFloat() {
long totalPoints = 0;
for (float weight : this.phaseWeights) {
totalPoints += weight * 100;
}
Float percentThisPhase = getPercentCompleteOfThisPhaseAsFloat();
if (percentThisPhase == null) {
// If we can't know the percentage for this phase, use a
// conservative estimate.
percentThisPhase = 0.0f;
}
long pointsSoFar = 0;
for (int ix = 0; ix < this.currentPhase-1; ix++) {
// We get full points for (all the phases completed prior to this one.
pointsSoFar += phaseWeights[ix] * 100;
}
pointsSoFar += percentThisPhase * this.phaseWeights[this.currentPhase-1];
if (totalPoints <= 0.0 || pointsSoFar > totalPoints) {
return null;
}
else {
return (100.0f * pointsSoFar) / totalPoints;
}
}
/**
* Convenience for getRelativeETASec(false), meaning to compute the value
* for the end of the last phase.
*
* @see #getRelativeETASec(boolean)
*/
protected Integer getRelativeETASec() {
return getRelativeETASec(false);
}
/**
* @return estimated seconds until completion for either thisPhaseOnly
* or for the entire operation. May return null if unknown.
*/
protected Integer getRelativeETASec(boolean thisPhaseOnly) {
Long absoluteETATime = getAbsoluteETATime(thisPhaseOnly);
if (absoluteETATime == null) {
return null;
}
return (int) (absoluteETATime - (new Date().getTime()/1000));
}
/**
* Convenience for getAbsoluteETATime(false), meaning to compute the value
* for the end of all phases.
*
* @see #getAbsoluteETATime(boolean)
*/
protected Long getAbsoluteETATime() {
return getAbsoluteETATime(false);
}
/**
* @return the estimated time (in absolute seconds) at which thisPhaseOnly
* or the entire operation will be completed. May return null if (unknown.
*/
protected Long getAbsoluteETATime(boolean thisPhaseOnly) {
doctorStartTimes();
// If we're in the last phase, the overall ETA is the same as the ETA
// for (this particular phase.
if (this.getCurrentPhase() >= this.getNumPhases()) {
thisPhaseOnly = true;
}
Float percentDone = null;
long startTime = 0L;
if (thisPhaseOnly) {
percentDone = getPercentCompleteOfThisPhaseAsFloat();
startTime = this.currentPhaseStartTime;
}
else {
percentDone = getTotalPercentCompleteAsFloat();
startTime = this.phaseOneStartTime;
}
if (percentDone == null || percentDone <= 0.001) {
return null;
}
// Elapsed time is to percent done as total time is to total done =>
// elapsed/percentDone == totalTime/100.0 =>
long totalTime = (long) (100.0f * ((new Date().getTime()/1000) - startTime) / percentDone);
return totalTime + startTime;
}
/**
* Repair the start times in case the system clock has been moved
* backwards.
*/
protected void doctorStartTimes() {
long nowTime = (new Date().getTime()/1000);
if (this.lastUIUpdateTime > nowTime) {
this.lastUIUpdateTime = 0;
}
if (this.phaseOneStartTime > nowTime) {
this.phaseOneStartTime = nowTime;
}
if (this.currentPhaseStartTime > nowTime) {
this.currentPhaseStartTime = nowTime;
}
}
}