[ui] use GNU wget's algorithms for rate/ETA display

This commit is contained in:
Pete Batard 2019-09-07 12:12:28 +01:00
parent 4c816a519e
commit 38ba8d366c
No known key found for this signature in database
GPG Key ID: 38E0CF5E69EDD671
4 changed files with 215 additions and 29 deletions

View File

@ -106,6 +106,10 @@ const char* additional_copyrights =
"https://www.gnu.org/software/fdisk\\line\n"
"GNU General Public License (GPL) v3 or later\\line\n"
"\\line\n"
"Speed/ETA computation from GNU wget:\\line\n"
"https://www.gnu.org/software/wget\\line\n"
"GNU General Public License (GPL) v3 or later\\line\n"
"\\line\n"
"Additional bootloaders from KolibriOS:\\line\n"
"https://kolibrios.org/\\line\n"
"GNU General Public License (GPL) v2 or later\\line\n"

View File

@ -33,7 +33,7 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_DIALOG DIALOGEX 12, 12, 232, 326
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_ACCEPTFILES
CAPTION "Rufus 3.7.1574"
CAPTION "Rufus 3.7.1575"
FONT 9, "Segoe UI Symbol", 400, 0, 0x0
BEGIN
LTEXT "Drive Properties",IDS_DRIVE_PROPERTIES_TXT,8,6,53,12,NOT WS_GROUP
@ -394,8 +394,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 3,7,1574,0
PRODUCTVERSION 3,7,1574,0
FILEVERSION 3,7,1575,0
PRODUCTVERSION 3,7,1575,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@ -413,13 +413,13 @@ BEGIN
VALUE "Comments", "https://akeo.ie"
VALUE "CompanyName", "Akeo Consulting"
VALUE "FileDescription", "Rufus"
VALUE "FileVersion", "3.7.1574"
VALUE "FileVersion", "3.7.1575"
VALUE "InternalName", "Rufus"
VALUE "LegalCopyright", "© 2011-2019 Pete Batard (GPL v3)"
VALUE "LegalTrademarks", "https://www.gnu.org/copyleft/gpl.html"
VALUE "OriginalFilename", "rufus-3.7.exe"
VALUE "ProductName", "Rufus"
VALUE "ProductVersion", "3.7.1574"
VALUE "ProductVersion", "3.7.1575"
END
END
BLOCK "VarFileInfo"

208
src/ui.c
View File

@ -44,7 +44,6 @@
UINT_PTR UM_LANGUAGE_MENU_MAX = UM_LANGUAGE_MENU;
HIMAGELIST hUpImageList, hDownImageList;
extern BOOL enable_fido, use_vds;
// TODO: Use an enum or something
int update_progress_type = UPT_PERCENT;
int advanced_device_section_height, advanced_format_section_height;
// (empty) check box width, (empty) drop down width, button height (for and without dropdown match)
@ -1264,25 +1263,134 @@ void UpdateProgress(int op, float percent)
}
}
/*
* The following is taken from GNU wget (progress.c)
*/
struct bar_progress {
uint64_t total_length; // expected total byte count when the download finishes
uint64_t count; // bytes downloaded so far
uint64_t last_screen_update; // time of the last screen update, measured since the beginning of download.
uint64_t dltime; // download time so far
// Keep track of recent download speeds.
struct bar_progress_hist {
uint64_t pos;
uint64_t times[SPEED_HISTORY_SIZE];
uint64_t bytes[SPEED_HISTORY_SIZE];
// The sum of times and bytes respectively, maintained for efficiency.
uint64_t total_time;
uint64_t total_bytes;
} hist;
uint64_t recent_start; // timestamp of beginning of current position.
uint64_t recent_bytes; // bytes downloaded so far.
BOOL stalled; // set when no data arrives for longer than STALL_START_TIME, then reset when new data arrives.
// The following are used to make sure that ETA information doesn't flicker.
uint64_t last_eta_time; // time of the last update to download speed and ETA, measured since the beginning of download.
int last_eta_value;
};
// This code attempts to maintain the notion of a "current" download speed, over the course
// of no less than 3s. (Shorter intervals produce very erratic results.)
//
// To do so, it samples the speed in 150ms intervals and stores the recorded samples in a
// FIFO history ring. The ring stores no more than 20 intervals, hence the history covers
// the period of at least three seconds and at most 20 reads into the past. This method
// should produce reasonable results for downloads ranging from very slow to very fast.
//
// The idea is that for fast downloads, we get the speed over exactly the last three seconds.
// For slow downloads (where a network read takes more than 150ms to complete), we get the
// speed over a larger time period, as large as it takes to complete twenty reads. This is
// good because slow downloads tend to fluctuate more and a 3-second average would be too
// erratic.
static void bar_update(struct bar_progress* bp, uint64_t howmuch, uint64_t dltime)
{
struct bar_progress_hist* hist = &bp->hist;
uint64_t recent_age = dltime - bp->recent_start;
// Update the download count.
bp->recent_bytes += howmuch;
// For very small time intervals, we return after having updated the
// "recent" download count. When its age reaches or exceeds minimum
// sample time, it will be recorded in the history ring.
if (recent_age < SPEED_SAMPLE_MIN)
return;
if (howmuch == 0) {
// If we're not downloading anything, we might be stalling,
// i.e. not downloading anything for an extended period of time.
// Since 0-reads do not enter the history ring, recent_age
// effectively measures the time since last read.
if (recent_age >= STALL_START_TIME) {
// If we're stalling, reset the ring contents because it's
// stale and because it will make bar_update stop printing
// the (bogus) current bandwidth.
bp->stalled = TRUE;
memset(hist, 0, sizeof(struct bar_progress_hist));
bp->recent_bytes = 0;
}
return;
}
// We now have a non-zero amount of to store to the speed ring.
// If the stall status was acquired, reset it.
if (bp->stalled) {
bp->stalled = FALSE;
// "recent_age" includes the entire stalled period, which
// could be very long. Don't update the speed ring with that
// value because the current bandwidth would start too small.
// Start with an arbitrary (but more reasonable) time value and
// let it level out.
recent_age = 1000;
}
// Store "recent" bytes and download time to history ring at the position POS.
// To correctly maintain the totals, first invalidate existing data
// (least recent in time) at this position. */
hist->total_time -= hist->times[hist->pos];
hist->total_bytes -= hist->bytes[hist->pos];
// Now store the new data and update the totals.
hist->times[hist->pos] = recent_age;
hist->bytes[hist->pos] = bp->recent_bytes;
hist->total_time += recent_age;
hist->total_bytes += bp->recent_bytes;
// Start a new "recent" period.
bp->recent_start = dltime;
bp->recent_bytes = 0;
// Advance the current ring position.
if (++hist->pos == SPEED_HISTORY_SIZE)
hist->pos = 0;
}
// This updates the progress bar as well as the data displayed on it so that we can
// display percentage completed, rate of transfer and estimated remaining duration.
// During init (op = OP_INIT) an optional HWND can be passed on which to look for
// a progress bar.
// a progress bar. Part of the code (eta, speed) comes from GNU wget.
void UpdateProgressWithInfo(int op, int msg, uint64_t processed, uint64_t total)
{
static int last_update_progress_type = UPT_PERCENT;
static struct bar_progress bp = { 0 };
HWND hProgressDialog = (HWND)(uintptr_t)processed;
static HWND hProgressBar = NULL;
static uint64_t start_time = 0, last_refresh = 0;
uint64_t rate = 0, current_time = GetTickCount64();
static float percent = 0.0f;
uint64_t speed = 0, current_time = GetTickCount64();
double percent = 0.0;
char msg_data[128];
static BOOL bNoAltMode = FALSE;
if (op == OP_INIT) {
start_time = current_time - 1;
last_refresh = 0;
last_update_progress_type = UPT_PERCENT;
percent = 0.0f;
rate = 0;
speed = 0;
memset(&bp, 0, sizeof(bp));
bp.total_length = total;
hProgressBar = NULL;
bNoAltMode = (BOOL)msg;
if (hProgressDialog != NULL) {
@ -1296,34 +1404,88 @@ void UpdateProgressWithInfo(int op, int msg, uint64_t processed, uint64_t total)
SendMessage(hProgressDialog, UM_PROGRESS_INIT, 0, 0);
}
} else if ((hProgressBar != NULL) || (op > 0)) {
if (processed > total)
processed = total;
percent = (100.0f * processed) / (1.0f * total);
// TODO: Better transfer rate computation using a weighted algorithm such as one from
// https://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately
rate = (current_time == start_time) ? 0 : (processed * 1000) / (current_time - start_time);
if ((processed == total) || (current_time > last_refresh + MAX_REFRESH)) {
if (bNoAltMode)
update_progress_type = 0;
if (update_progress_type == UPT_SPEED) {
static_sprintf(msg_data, "%s/s", SizeToHumanReadable(rate, FALSE, FALSE));
} else if (update_progress_type == UPT_TIME) {
uint64_t seconds = (rate == 0) ? 24 * 3600 : (total - processed) / rate + 1;
static_sprintf(msg_data, "%d:%02d:%02d", (uint32_t)(seconds / 3600), (uint16_t)((seconds % 3600) / 60), (uint16_t)(seconds % 60));
uint64_t dl_total_time = current_time - start_time;
uint64_t howmuch = processed - bp.count;
bp.count = processed;
bp.total_length = total;
if (bp.count > bp.total_length)
bp.total_length = bp.count;
if (bp.total_length > 0)
percent = (100.0f * bp.count) / (1.0f * bp.total_length);
else
percent = 0.0f;
if ((bp.hist.total_time > 999) && (bp.hist.total_bytes != 0)) {
// Calculate the download speed using the history ring and
// recent data that hasn't made it to the ring yet.
uint64_t dlquant = bp.hist.total_bytes + bp.recent_bytes;
uint64_t dltime = bp.hist.total_time + (dl_total_time - bp.recent_start);
speed = (dltime == 0) ? 0 : (dlquant * 1000) / dltime;
} else {
speed = 0;
}
bar_update(&bp, howmuch, dl_total_time);
if (bNoAltMode)
update_progress_type = UPT_PERCENT;
switch (update_progress_type) {
case UPT_SPEED:
if (speed != 0)
static_sprintf(msg_data, "%s/s", SizeToHumanReadable(speed, FALSE, FALSE));
else
static_sprintf(msg_data, "---");
break;
case UPT_ETA:
if ((bp.total_length > 0) && (bp.count > 0) && (dl_total_time > 3000)) {
uint32_t eta = 0;
// Don't change the value of ETA more than approximately once
// per second; doing so would cause flashing without providing
// any value to the user.
if ((bp.total_length != processed) && (bp.last_eta_value != 0) &&
(dl_total_time - bp.last_eta_time < ETA_REFRESH_INTERVAL)) {
eta = bp.last_eta_value;
} else {
// Calculate ETA using the average download speed to predict
// the future speed. If you want to use a speed averaged
// over a more recent period, replace dl_total_time with
// hist->total_time and bp->count with hist->total_bytes.
// I found that doing that results in a very jerky and
// ultimately unreliable ETA.
uint64_t bytes_remaining = bp.total_length - processed;
double d_eta = (dl_total_time / 1000.0) * (bytes_remaining * 1.0) / (bp.count * 1.0);
if (d_eta >= INT_MAX - 1)
goto skip_eta;
eta = (uint32_t)(d_eta + 0.5);
bp.last_eta_value = eta;
bp.last_eta_time = dl_total_time;
}
static_sprintf(msg_data, "%d:%02d:%02d", eta / 3600, (uint16_t)((eta % 3600) / 60), (uint16_t)(eta % 60));
} else {
static_sprintf(msg_data, "%0.1f%%", percent);
skip_eta:
static_sprintf(msg_data, "-:--:--");
}
last_refresh = current_time;
break;
default:
static_sprintf(msg_data, "%0.1f%%", percent);
break;
}
if ((bp.count == bp.total_length) || (current_time > last_refresh + MAX_REFRESH)) {
if (op < 0) {
SendMessage(hProgressBar, PBM_SETPOS, (WPARAM)(MAX_PROGRESS * percent / 100.0f), 0);
if (op == OP_NOOP_WITH_TASKBAR)
SetTaskbarProgressValue((ULONGLONG)(MAX_PROGRESS * percent / 100.0f), MAX_PROGRESS);
} else {
UpdateProgress(op, percent);
UpdateProgress(op, (float)percent);
}
if (msg >= 0)
if ((msg >= 0) && ((current_time > bp.last_screen_update + SCREEN_REFRESH_INTERVAL) ||
(last_update_progress_type != update_progress_type) || (bp.count == bp.total_length))) {
PrintInfo(0, msg, msg_data);
bp.last_screen_update = current_time;
}
last_refresh = current_time;
}
last_update_progress_type = update_progress_type;
}
}

View File

@ -47,10 +47,30 @@
enum update_progress_type {
UPT_PERCENT = 0,
UPT_SPEED,
UPT_TIME,
UPT_ETA,
UPT_MAX
};
// Size of the download speed history ring.
#define SPEED_HISTORY_SIZE 20
// The minimum time length of a history sample. By default, each sample is at least 150ms long,
// which means that, over the course of 20 samples, "current" download speed spans at least 3s
// into the past.
#define SPEED_SAMPLE_MIN 150
// The time after which the download starts to be considered "stalled", i.e. the current
// bandwidth is not printed and the recent download speeds are scratched.
#define STALL_START_TIME 5000
// Time between screen refreshes will not be shorter than this.
// NB: In Rufus' case, "screen" means the text overlaid on the progress bar.
#define SCREEN_REFRESH_INTERVAL 200
// Don't refresh the ETA too often to avoid jerkiness in predictions.
// This allows ETA to change approximately once per second.
#define ETA_REFRESH_INTERVAL 990
extern HWND hMultiToolbar, hSaveToolbar, hHashToolbar, hAdvancedDeviceToolbar, hAdvancedFormatToolbar;
extern HFONT hInfoFont;
extern UINT_PTR UM_LANGUAGE_MENU_MAX;