/* * Copyright (C) 2014 Jörg Prante * * 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 org.xbib.io; import java.util.Map; import java.util.TreeMap; /** * Utility class that tracks the number of bytes transferred from a source, and * uses this information to calculate transfer rates and estimate end times. The * watcher stores the number of bytes that will be transferred, the number of * bytes that have been transferred in the current session and the time this has * taken, and the number of bytes and time taken overal (eg for transfers that * have been restarted). */ public class BytesProgressWatcher { /** * The number of seconds worth of historical byte transfer information that * will be stored and used to calculate the recent transfer rate. */ public static final int SECONDS_OF_HISTORY = 5; private boolean isStarted = false; private long bytesToTransfer = 0; private long startTimeAllTransfersMS = -1; private long totalBytesInAllTransfers = 0; private long startTimeCurrentTransferMS = -1; private long totalBytesInCurrentTransfer = 0; private long endTimeCurrentTransferMS = -1; private Map<Long, Long> historyOfBytesBySecond = new TreeMap<Long, Long>(); private long earliestHistorySecond = Long.MAX_VALUE; /** * Construct a watcher for a transfer that will involve a given number of * bytes. * * @param bytesToTransfer the number of bytes that will be transferred, eg * the size of a file being uploaded. */ public BytesProgressWatcher(long bytesToTransfer) { this.bytesToTransfer = bytesToTransfer; } /** * @return the count of bytes that will be transferred by the object watched * by this class. */ public synchronized long getBytesToTransfer() { return bytesToTransfer; } /** * Resets the byte count and timer variables for a watcher. This method is * called automatically when a transfer is started (ie the first bytes are * registered in the method {@link #updateBytesTransferred(long)}), or when * a transfer is restarted (eg due to transmission errors). */ public synchronized void resetWatcher() { startTimeCurrentTransferMS = System.currentTimeMillis(); if (startTimeAllTransfersMS == -1) { startTimeAllTransfersMS = startTimeCurrentTransferMS; } endTimeCurrentTransferMS = -1; totalBytesInCurrentTransfer = 0; isStarted = true; } /** * Notifies this watcher that bytes have been transferred. * * @param byteCount the number of bytes that have been transferred. */ public synchronized void updateBytesTransferred(long byteCount) { // Start the monitor when we are notified of the first bytes transferred. if (!isStarted) { resetWatcher(); } // Store the total byte count for the current transfer, and for all transfers. totalBytesInCurrentTransfer += byteCount; totalBytesInAllTransfers += byteCount; // Recognise when all the expected bytes have been transferred and mark the end time. if (totalBytesInCurrentTransfer >= bytesToTransfer) { endTimeCurrentTransferMS = System.currentTimeMillis(); } // Keep historical records of the byte counts transferred in a given second. Long currentSecond = System.currentTimeMillis() / 1000; Long bytesInSecond = historyOfBytesBySecond.get(currentSecond); if (bytesInSecond != null) { historyOfBytesBySecond.put(currentSecond, byteCount + bytesInSecond); } else { historyOfBytesBySecond.put(currentSecond, byteCount); } // Remember the earliest second value for which we have historical info. if (currentSecond < earliestHistorySecond) { earliestHistorySecond = currentSecond; } // Remove any history records we are no longer interested in. long removeHistoryBeforeSecond = currentSecond - SECONDS_OF_HISTORY; for (long sec = earliestHistorySecond; sec < removeHistoryBeforeSecond; sec++) { historyOfBytesBySecond.remove(sec); } earliestHistorySecond = removeHistoryBeforeSecond; } /** * @return the number of bytes that have so far been transferred in the most * recent transfer session. */ public synchronized long getBytesTransferred() { return totalBytesInCurrentTransfer; } /** * @return the number of bytes that are remaining to be transferred. */ public synchronized long getBytesRemaining() { return bytesToTransfer - totalBytesInCurrentTransfer; } /** * @return an estimate of the time (in seconds) it will take for the * transfer to completed, based on the number of bytes remaining to transfer * and the overall bytes/second rate. */ public synchronized long getRemainingTime() { BytesProgressWatcher[] progressWatchers = new BytesProgressWatcher[1]; progressWatchers[0] = this; long bytesRemaining = bytesToTransfer - totalBytesInCurrentTransfer; double remainingSecs = (double) bytesRemaining / calculateOverallBytesPerSecond(progressWatchers); return Math.round(remainingSecs); } /** * @return the byte rate (per second) based on the historical information * for the last * {@link #SECONDS_OF_HISTORY} seconds before the current time. */ public synchronized double getRecentByteRatePerSecond() { if (!isStarted) { return 0; } long currentSecond = System.currentTimeMillis() / 1000; long startSecond = 1 + (currentSecond - SECONDS_OF_HISTORY); long endSecond = (endTimeCurrentTransferMS != -1 ? endTimeCurrentTransferMS / 1000 : currentSecond); if (currentSecond - SECONDS_OF_HISTORY > endSecond) { // This item finished too long ago, ignore it now. historyOfBytesBySecond.clear(); return 0; } // Count the number of bytes transferred from SECONDS_OF_HISTORY ago to the second before now. long sumOfBytes = 0; long numberOfSecondsInHistory = 0; for (long sec = startSecond; sec <= endSecond; sec++) { numberOfSecondsInHistory++; Long bytesInSecond = historyOfBytesBySecond.get(sec); if (bytesInSecond != null) { sumOfBytes += bytesInSecond; } } return (numberOfSecondsInHistory == 0 ? 0 : (double) sumOfBytes / numberOfSecondsInHistory); } /** * @return the number of milliseconds time elapsed for a transfer. The value * returned is the time elapsed so far if the transfer is ongoing, the total * time taken for the transfer if it is complete, or 0 if the transfer has * not yet started. */ public synchronized long getElapsedTimeMS() { if (!isStarted) { return 0; } if (endTimeCurrentTransferMS != -1) { // Transfer is complete, report the time it took. return endTimeCurrentTransferMS - startTimeCurrentTransferMS; } else { return System.currentTimeMillis() - startTimeCurrentTransferMS; } } /** * @return the number of bytes that have been transferred over all sessions, * including any sessions that have been restarted. */ public synchronized long getTotalBytesInAllTransfers() { return totalBytesInAllTransfers; } protected synchronized boolean isStarted() { return isStarted; } /** * @return the time (in milliseconds) when the first bytes were transferred, * regardless of how many times the transfer was reset. */ public synchronized long getHistoricStartTimeMS() { return startTimeAllTransfersMS; } /** * @param progressWatchers all the watchers involved in the same byte * transfer operation. * @return the total number of bytes to transfer. */ public static long sumBytesToTransfer(BytesProgressWatcher[] progressWatchers) { long sumOfBytes = 0; for (BytesProgressWatcher progressWatcher : progressWatchers) { sumOfBytes += progressWatcher.getBytesToTransfer(); } return sumOfBytes; } /** * @param progressWatchers all the watchers involved in the same byte * transfer operation. * @return the total number of bytes already transferred. */ public static long sumBytesTransferred(BytesProgressWatcher[] progressWatchers) { long sumOfBytes = 0; for (BytesProgressWatcher progressWatcher : progressWatchers) { sumOfBytes += progressWatcher.getBytesTransferred(); } return sumOfBytes; } /** * @param progressWatchers all the watchers involved in the same byte * transfer operation. * @return an estimate of the time (in seconds) it will take for the * transfer to completed, based on the number of bytes remaining to transfer * and the overall bytes/second rate. */ public static long calculateRemainingTime(BytesProgressWatcher[] progressWatchers) { long bytesRemaining = sumBytesToTransfer(progressWatchers) - sumBytesTransferred(progressWatchers); double bytesPerSecond = calculateOverallBytesPerSecond(progressWatchers); if (Math.abs(bytesPerSecond) < 0.001d) { // No transfer has occurred yet. return 0; } double remainingSecs = (double) bytesRemaining / bytesPerSecond; return Math.round(remainingSecs); } /** * @param progressWatchers all the watchers involved in the same byte * transfer operation. * @return the overall rate of bytes/second over all transfers for all * watchers. */ public static double calculateOverallBytesPerSecond(BytesProgressWatcher[] progressWatchers) { long initialStartTime = Long.MAX_VALUE; // The oldest start time of any monitor. long bytesTotal = 0; for (BytesProgressWatcher progressWatcher : progressWatchers) { // Ignore any watchers that have not yet started. if (!progressWatcher.isStarted()) { continue; } // Add up all the bytes transferred by all started watchers. bytesTotal += progressWatcher.getTotalBytesInAllTransfers(); // Find the earliest starting time of any monitor. if (progressWatcher.getHistoricStartTimeMS() < initialStartTime) { initialStartTime = progressWatcher.getHistoricStartTimeMS(); } } // Determine how much time has elapsed since the earliest watcher start time. long elapsedTimeSecs = (System.currentTimeMillis() - initialStartTime) / 1000; // Calculate the overall rate of bytes/second over all transfers for all watchers. return elapsedTimeSecs == 0 ? bytesTotal : (double) bytesTotal / elapsedTimeSecs; } /** * @param progressWatchers all the watchers involved in the same byte * transfer operation. * @return the rate of bytes/second that has been achieved recently (ie * within the last * {@link #SECONDS_OF_HISTORY} seconds). */ public static long calculateRecentByteRatePerSecond(BytesProgressWatcher[] progressWatchers) { double sumOfRates = 0; for (BytesProgressWatcher progressWatcher : progressWatchers) { if (progressWatcher.isStarted()) { sumOfRates += progressWatcher.getRecentByteRatePerSecond(); } } return Math.round(sumOfRates); } }