Files
wget2/libwget/bar.c
Darshit Shah 08d0eeabe7 Rewrte the speed reporting codepath
* include/wget/wget.h (wget_human_readable): Function is not a const
* libwget/bar.c (_BAR_SETTINGS): Remove SPEED_REDRAW_TIME
(struct _speed_report): Remove and add relevant fields to _bar_slot_t
(_bar_update_speed): Removed
(_bar_update_speed_stats): New function to populate the speed ring and
fill in the speed_buf buffer
(_bar_set_progress): Remove old commented out code
(_bar_update_slot): Speed computation is moved to
`_bar_update_speed_stats`.
(wget_bar_set_slots): No longer need to realloc speed ring buffers
(wget_bar_slot_begin): Initialize the ring_pos and the speed rings
(wget_bar_deinit): Variable `speed_r` no longer used

In this implementation, I've made a few functional changes and some
other design changes to how the speed report is computed. First, I've
removed the assumption that the progress bar is updated every 125ms.
This is part of the client side logic and we can't assume that in the
library. Instead, the speed report is computed every time the progress
bar is refreshed. The size of the speed ring and how the computation is
done is documented to allow the library users to make a decision on how
they want to use the progress bar.

While, I've currently removed SPEED_REDRAW_TIME, I do plan to bring it
back soon. This will most likely be done alongside support for computing
the ETA of the download.

In this commit, I have also made it such that the speed is shown only
after the speed ring is filled in entirely. This most likely needs to
change such that we show the speed immediately. However, we document
that it will jitter a lot while the speed ring is being populated.
2018-07-29 10:31:18 +02:00

617 lines
16 KiB
C

/*
* Copyright(c) 2014 Tim Ruehsen
* Copyright(c) 2015-2018 Free Software Foundation, Inc.
*
* This file is part of libwget.
*
* Libwget is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Libwget is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libwget. If not, see <https://www.gnu.org/licenses/>.
*
*
* Progress bar routines
*
* Changelog
* 18.10.2014 Tim Ruehsen created from src/bar.c
*
*/
#include <config.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/time.h>
#include <signal.h>
#include <wget.h>
#include "private.h"
/**
* \file
* \brief Progress Bar Routines
* \defgroup libwget-progress Progress Display Functions
* @{
*
* Methods for creating and printing a progress bar display.
*/
// We use enums to define the progress bar parameters because they are the
// closest thing we have to defining true constants in C without using
// preprocessor macros. The advantage of enums is that they will create a
// symbol in the symbol table making debugging a whole lot easier.
// Define the parameters for how the progress bar looks
enum _BAR_SIZES {
_BAR_FILENAME_SIZE = 20,
_BAR_RATIO_SIZE = 3,
_BAR_METER_COST = 2,
_BAR_DOWNBYTES_SIZE = 8,
_BAR_SPEED_SIZE = 8,
};
// Define the cost (in number of columns) of the progress bar decorations. This
// includes all the elements that are not the progress indicator itself.
enum _BAR_DECOR_SIZE {
_BAR_DECOR_COST =
_BAR_FILENAME_SIZE + 1 + \
_BAR_RATIO_SIZE + 2 + \
_BAR_METER_COST + 1 + \
_BAR_DOWNBYTES_SIZE + 1 + \
_BAR_SPEED_SIZE + 3
};
enum _SCREEN_WIDTH {
DEFAULT_SCREEN_WIDTH = 70,
MINIMUM_SCREEN_WIDTH = 45,
};
enum _bar_slot_status_t {
EMPTY = 0,
DOWNLOADING = 1,
COMPLETE = 2
};
/** The settings for drawing the progress bar.
*
* This includes things like how often it is updated, how many values are
* stored in the speed ring, etc.
*/
enum _BAR_SETTINGS {
/// The number of values to store in the speed ring
SPEED_RING_SIZE = 24,
};
typedef struct {
char
*progress,
*filename,
speed_buf[_BAR_SPEED_SIZE],
human_size[_BAR_DOWNBYTES_SIZE];
uint64_t
file_size,
time_ring[SPEED_RING_SIZE],
bytes_ring[SPEED_RING_SIZE],
bytes_downloaded;
int
ring_pos,
tick;
enum _bar_slot_status_t
status;
bool
redraw : 1;
} _bar_slot_t;
struct _wget_bar_st {
_bar_slot_t
*slots;
char
*progress_mem_holder,
*unknown_size,
*known_size,
*spaces;
int
nslots,
max_width;
wget_thread_mutex_t
mutex;
};
static char report_speed_type = WGET_REPORT_SPEED_BYTES;
static char report_speed_type_char = 'B';
static unsigned short speed_modifier = 1000;
// The progress bar may be redrawn if the window size changes.
// XXX: Don't handle that case currently. Instead, later test
// what happens if we don't explicitly redraw in such a case.
// For fast downloads, it doesn't matter. For slow downloads,
// the progress bar will maybe span across two lines till it
// gets redrawn. Ideally, this should be a part of the client
// code logic and not in the library.
// Tl;dr: Move window size detection to client. Allow client to
// specify rate at which speed stats should be updated. Speed
// ring size will remain constant (Don't want second heap allocation)
// - darnir 29/07/2018
static void _bar_update_speed_stats(_bar_slot_t *slotp)
{
int ring_pos = slotp->ring_pos;
// In case this function is called with no downloaded bytes,
// exit early
if (slotp->bytes_downloaded == slotp->bytes_ring[ring_pos]) {
return;
}
uint64_t curtime = wget_get_timemillis();
// Increment the position pointer
if (++ring_pos == SPEED_RING_SIZE)
ring_pos = 0;
slotp->bytes_ring[ring_pos] = slotp->bytes_downloaded;
slotp->time_ring[ring_pos] = curtime;
int next_pos = (ring_pos + 1 == SPEED_RING_SIZE) ? 0 : ring_pos + 1;
if (slotp->bytes_ring[next_pos] == 0) {
sprintf(slotp->speed_buf, " --.-K");
} else {
size_t bytes = slotp->bytes_ring[ring_pos] - slotp->bytes_ring[next_pos];
size_t time = slotp->time_ring[ring_pos] - slotp->time_ring[next_pos];
size_t speed = (bytes * speed_modifier) / time;
wget_human_readable(slotp->speed_buf, sizeof(slotp->speed_buf), speed);
}
slotp->ring_pos = ring_pos;
}
static volatile sig_atomic_t winsize_changed;
static inline G_GNUC_WGET_ALWAYS_INLINE void
_restore_cursor_position(void)
{
// CSI u: Restore cursor position
fputs("\033[u", stdout);
}
static inline G_GNUC_WGET_ALWAYS_INLINE void
_bar_print_slot(const wget_bar_t *bar, int slot)
{
// CSI s: Save cursor
// CSI <n> A: Cursor up
// CSI <n> G: Cursor horizontal absolute
wget_fprintf(stdout, "\033[s\033[%dA\033[1G", bar->nslots - slot);
}
static inline G_GNUC_WGET_ALWAYS_INLINE void
_bar_set_progress(const wget_bar_t *bar, int slot)
{
_bar_slot_t *slotp = &bar->slots[slot];
if (slotp->file_size > 0) {
size_t bytes = slotp->bytes_downloaded;
int cols = (int) ((bytes / (double) slotp->file_size) * bar->max_width);
if (cols > bar->max_width)
cols = bar->max_width;
else if (cols <= 0)
cols = 1;
// Write one extra byte for \0. This has already been accounted for
// when initializing the progress storage.
memcpy(slotp->progress, bar->known_size, cols - 1);
slotp->progress[cols - 1] = '>';
if (cols < bar->max_width)
memset(slotp->progress + cols, ' ', bar->max_width - cols);
} else {
int ind = slotp->tick % (bar->max_width * 2 - 6);
int pre_space;
if (ind <= bar->max_width - 3)
pre_space = ind;
else
pre_space = bar->max_width - (ind - bar->max_width + 5);
memset(slotp->progress, ' ', bar->max_width);
memcpy(slotp->progress + pre_space, "<=>", 3);
}
slotp->progress[bar->max_width] = 0;
}
static void _bar_update_slot(const wget_bar_t *bar, int slot)
{
_bar_slot_t *slotp = &bar->slots[slot];
// We only print a progress bar for the slot if a context has been
// registered for it
if (slotp->status == DOWNLOADING || slotp->status == COMPLETE) {
uint64_t max, cur;
int ratio;
max = slotp->file_size;
cur = slotp->bytes_downloaded;
ratio = max ? (int) ((100 * cur) / max) : 0;
wget_human_readable(slotp->human_size, sizeof(slotp->human_size), cur);
_bar_update_speed_stats(slotp);
_bar_set_progress(bar, slot);
_bar_print_slot(bar, slot);
// The progress bar looks like this:
//
// filename xxx% [======> ] xxx.xxK
//
// It is made of the following elements:
// filename _BAR_FILENAME_SIZE Name of local file
// xxx% _BAR_RATIO_SIZE + 1 Amount of file downloaded
// [] _BAR_METER_COST Bar Decorations
// xxx.xxK _BAR_DOWNBYTES_SIZE Number of downloaded bytes
// xxx.xxKB/s _BAR_SPEED_SIZE Download speed
// ===> Remaining Progress Meter
wget_fprintf(stdout, "%-*.*s %*d%% [%s] %*s %*s%c/s",
_BAR_FILENAME_SIZE, _BAR_FILENAME_SIZE, slotp->filename,
_BAR_RATIO_SIZE, ratio,
slotp->progress,
_BAR_DOWNBYTES_SIZE, slotp->human_size,
_BAR_SPEED_SIZE, slotp->speed_buf, report_speed_type_char);
_restore_cursor_position();
fflush(stdout);
slotp->tick++;
}
}
static int _bar_get_width(void)
{
int width = DEFAULT_SCREEN_WIDTH;
if (wget_get_screen_size(&width, NULL) == 0) {
if (width < MINIMUM_SCREEN_WIDTH)
width = MINIMUM_SCREEN_WIDTH;
else
width--; // leave one space at the end, else we see a linebreak on Windows
}
return width - _BAR_DECOR_COST;
}
static void _bar_update_winsize(wget_bar_t *bar, bool slots_changed) {
if (winsize_changed || slots_changed) {
int max_width = _bar_get_width();
if (bar->max_width < max_width) {
xfree(bar->known_size);
bar->known_size = xmalloc(max_width);
memset(bar->known_size, '=', max_width);
xfree(bar->unknown_size);
bar->unknown_size = xmalloc(max_width);
memset(bar->unknown_size, '*', max_width);
xfree(bar->spaces);
bar->spaces = xmalloc(max_width);
memset(bar->spaces, ' ', max_width);
}
if (bar->max_width < max_width || slots_changed) {
xfree(bar->progress_mem_holder);
// Add one extra byte to hold the \0 character
bar->progress_mem_holder = xcalloc(bar->nslots, max_width + 1);
for (int i = 0; i < bar->nslots; i++) {
bar->slots[i].progress = bar->progress_mem_holder + (i * max_width);
}
}
bar->max_width = max_width;
}
winsize_changed = 0;
}
static void _bar_update(wget_bar_t *bar)
{
_bar_update_winsize(bar, false);
for (int i = 0; i < bar->nslots; i++) {
if (bar->slots[i].redraw || winsize_changed) {
_bar_update_slot(bar, i);
bar->slots[i].redraw = 0;
}
}
}
/**
* \param[in] bar Pointer to a \p wget_bar_t object
* \param[in] nslots Number of progress bars
* \return Pointer to a \p wget_bar_t object
*
* Initialize a new progress bar instance for Wget. If \p bar is a NULL
* pointer, it will be allocated on the heap and a pointer to the newly
* allocated memory will be returned. To free this memory, call either the
* wget_bar_deinit() or wget_bar_free() functions based on your needs.
*
* \p nslots is the number of screen lines to reserve for printing the progress
* bars. This may be any number, but you generally want at least as many slots
* as there are downloader threads.
*/
wget_bar_t *wget_bar_init(wget_bar_t *bar, int nslots)
{
/* Initialize screen_width if this hasn't been done or if it might
have changed, as indicated by receiving SIGWINCH. */
int max_width = _bar_get_width();
if (nslots < 1 || max_width < 1)
return NULL;
if (!bar)
bar = xcalloc(1, sizeof(*bar));
else
memset(bar, 0, sizeof(*bar));
wget_thread_mutex_init(&bar->mutex);
wget_bar_set_slots(bar, nslots);
return bar;
}
/**
* \param[in] bar Pointer to a wget_bar_t object
* \param[in] nslots The new number of progress bars that should be drawn
*
* Update the number of progress bar lines that are drawn on the screen.
* This is useful when the number of downloader threads changes dynamically or
* to change the number of reserved lines. Calling this function will
* immediately reserve \p nslots lines on the screen. However if \p nslots is
* lower than the existing value, nothing will be done.
*/
void wget_bar_set_slots(wget_bar_t *bar, int nslots)
{
wget_thread_mutex_lock(bar->mutex);
int more_slots = nslots - bar->nslots;
if (more_slots > 0) {
bar->slots = wget_realloc(bar->slots, nslots * sizeof(_bar_slot_t));
memset(bar->slots + bar->nslots, 0, more_slots * sizeof(_bar_slot_t));
bar->nslots = nslots;
for (int i = 0; i < more_slots; i++)
fputs("\n", stdout);
_bar_update_winsize(bar, true);
_bar_update(bar);
}
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to a wget_bar_t object
* \param[in] slot The slot number to use
* \param[in] filename The file name to display in the given \p slot
* \param[in] file_size The file size that would be 100%
*
* Initialize the given \p slot of the \p bar object with it's (file) name to display
* and the (file) size to be assumed 100%.
*/
void wget_bar_slot_begin(wget_bar_t *bar, int slot, const char *filename, ssize_t file_size)
{
wget_thread_mutex_lock(bar->mutex);
_bar_slot_t *slotp = &bar->slots[slot];
xfree(slotp->filename);
slotp->filename = wget_strdup(filename);
slotp->tick = 0;
slotp->file_size = file_size;
slotp->bytes_downloaded = 0;
slotp->status = DOWNLOADING;
slotp->redraw = 1;
slotp->ring_pos = 0;
memset(&slotp->time_ring, 0, sizeof(slotp->time_ring));
memset(&slotp->bytes_ring, 0, sizeof(slotp->bytes_ring));
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to a wget_bar_t object
* \param[in] slot The slot number to use
* \param[in] nbytes The current number of bytes to display
*
* Set the current number of bytes for \p slot for the next update of
* the bar/slot.
*/
void wget_bar_slot_downloaded(wget_bar_t *bar, int slot, size_t nbytes)
{
wget_thread_mutex_lock(bar->mutex);
bar->slots[slot].bytes_downloaded = nbytes;
bar->slots[slot].redraw = 1;
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to a wget_bar_t object
* \param[in] slot The slot number to use
*
* Redraw the given \p slot as being completed.
*/
void wget_bar_slot_deregister(wget_bar_t *bar, int slot)
{
wget_thread_mutex_lock(bar->mutex);
if (slot >= 0 && slot < bar->nslots) {
_bar_slot_t *slotp = &bar->slots[slot];
slotp->status = COMPLETE;
_bar_update_slot(bar, slot);
}
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to a wget_bar_t object
*
* Redraw the parts of the \p bar that have been changed so far.
*/
void wget_bar_update(wget_bar_t *bar)
{
wget_thread_mutex_lock(bar->mutex);
_bar_update(bar);
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to \p wget_bar_t
*
* Free the various progress bar data structures
* without freeing \p bar itself.
*/
void wget_bar_deinit(wget_bar_t *bar)
{
if (bar) {
for (int i = 0; i < bar->nslots; i++) {
xfree(bar->slots[i].filename);
}
xfree(bar->progress_mem_holder);
xfree(bar->spaces);
xfree(bar->known_size);
xfree(bar->unknown_size);
xfree(bar->slots);
wget_thread_mutex_destroy(&bar->mutex);
}
}
/**
* \param[in] bar Pointer to \p wget_bar_t
*
* Free the various progress bar data structures
* including the \p bar pointer itself.
*/
void wget_bar_free(wget_bar_t **bar)
{
if (bar) {
wget_bar_deinit(*bar);
xfree(*bar);
}
}
/**
* \param[in] bar Pointer to \p wget_bar_t
* \param[in] slot The slot number to use
* \param[in] display The string to be displayed in the given slot
*
* Displays the \p display string in the given \p slot.
*/
void wget_bar_print(wget_bar_t *bar, int slot, const char *display)
{
wget_thread_mutex_lock(bar->mutex);
_bar_print_slot(bar, slot);
// CSI <n> G: Cursor horizontal absolute
wget_fprintf(stdout, "\033[27G[%-*.*s]", bar->max_width, bar->max_width, display);
_restore_cursor_position();
fflush(stdout);
wget_thread_mutex_unlock(bar->mutex);
}
/**
* \param[in] bar Pointer to \p wget_bar_t
* \param[in] slot The slot number to use
* \param[in] fmt Printf-like format to build the display string
* \param[in] args Arguments matching the \p fmt format string
*
* Displays the \p string build using the printf-style \p fmt and \p args.
*/
void wget_bar_vprintf(wget_bar_t *bar, int slot, const char *fmt, va_list args)
{
char text[bar->max_width + 1];
wget_vsnprintf(text, sizeof(text), fmt, args);
wget_bar_print(bar, slot, text);
}
/**
* \param[in] bar Pointer to \p wget_bar_t
* \param[in] slot The slot number to use
* \param[in] fmt Printf-like format to build the display string
* \param[in] ... List of arguments to match \p fmt
*
* Displays the \p string build using the printf-style \p fmt and the given arguments.
*/
void wget_bar_printf(wget_bar_t *bar, int slot, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
wget_bar_vprintf(bar, slot, fmt, args);
va_end(args);
}
/**
* Call this function when a resize of the screen / console has been detected.
*/
void wget_bar_screen_resized(void)
{
winsize_changed = 1;
}
/**
*
* \param[in] bar Pointer to \p wget_bar_t
* @param buf Pointer to buffer to be displayed
* @param len Number of bytes to be displayed
*
* Write 'above' the progress bar area, scrolls screen one line up
* if needed. Currently used by Wget2 to display error messages in
* color red.
*
* This function needs a redesign to be useful for general purposes.
*/
void wget_bar_write_line(wget_bar_t *bar, const char *buf, size_t len)
{
wget_thread_mutex_lock(bar->mutex);
// CSI s: Save cursor
// CSI <n>S: Scroll up whole screen
// CSI <n>A: Cursor up
// CSI <n>G: Cursor horizontal absolute
// CSI 0J: Clear from cursor to end of screen
// CSI 31m: Red text color
wget_fprintf(stdout, "\033[s\033[1S\033[%dA\033[1G\033[0J\033[31m", bar->nslots + 1);
fwrite(buf, 1, len, stdout);
fputs("\033[m", stdout); // reset text color
_restore_cursor_position();
_bar_update(bar);
wget_thread_mutex_unlock(bar->mutex);
}
/**
* @param type Report speed type
*
* Set the progress bar report speed type to WGET_REPORT_SPEED_BYTES
* or WGET_REPORT_SPEED_BITS.
*
* Default is WGET_REPORT_SPEED_BYTES.
*/
void wget_bar_set_speed_type(char type)
{
report_speed_type = type;
if (type == WGET_REPORT_SPEED_BITS) {
report_speed_type_char = 'b';
speed_modifier = 8;
}
}
/** @}*/