From 4d4c737dcb271b391841b6e08ff9b4e5326af663 Mon Sep 17 00:00:00 2001 From: Leonid Evdokimov Date: Tue, 11 Mar 2025 14:49:10 +0300 Subject: [PATCH 1/2] f3brew: write minimal progressbar to stdout --- Makefile | 2 +- f3brew.c | 26 ++++++++- libeta.c | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ libeta.h | 20 +++++++ 4 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 libeta.c create mode 100644 libeta.h diff --git a/Makefile b/Makefile index 82b2d72..37c3b9e 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ f3read: utils.o libflow.o f3read.o f3probe: libutils.o libdevs.o libprobe.o f3probe.o $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev -f3brew: libutils.o libdevs.o f3brew.o +f3brew: libutils.o libdevs.o libeta.o f3brew.o $(CC) -o $@ $^ $(LDFLAGS) -lm -ludev f3fix: libutils.o f3fix.o diff --git a/f3brew.c b/f3brew.c index 34047b2..d6f0266 100644 --- a/f3brew.c +++ b/f3brew.c @@ -10,6 +10,7 @@ #include "version.h" #include "libutils.h" #include "libdevs.h" +#include "libeta.h" /* Argp's global variables. */ const char *argp_program_version = "F3 BREW " F3_STR_VERSION; @@ -228,8 +229,11 @@ static void write_blocks(struct device *dev, char *flush_blk = buffer + BIG_BLOCK_SIZE_BYTE; uint64_t offset = first_block << block_order; uint64_t pos, first_pos = first_block; + struct etabar bar; assert(BIG_BLOCK_SIZE_BYTE >= block_size); + eta_init(&bar, last_block - first_block + 1); + eta_print(&bar); for (pos = first_block; pos <= last_block; pos++) { fill_buffer_with_block(stamp_blk, block_order, offset, 0); @@ -237,13 +241,23 @@ static void write_blocks(struct device *dev, offset += block_size; if (stamp_blk == flush_blk || pos == last_block) { - if (dev_write_blocks(dev, buffer, first_pos, pos)) + int rc = dev_write_blocks(dev, buffer, first_pos, pos); + int errrc = errno; + eta_stamp(&bar, pos - first_block + 1); + if (!rc) { + eta_redraw(&bar); + } else { + eta_clear(); + errno = errrc; warn("Failed to write blocks from 0x%" PRIx64 " to 0x%" PRIx64, first_pos, pos); + eta_print(&bar); + } stamp_blk = buffer; first_pos = pos + 1; } } + eta_clear(); } /* XXX Properly handle return errors. */ @@ -375,8 +389,10 @@ static void read_blocks(struct device *dev, .end_sector_offset = 0, .found_sector_offset = 0, }; + struct etabar bar; assert(BIG_BLOCK_SIZE_BYTE >= block_size); + eta_init(&bar, last_block - first_block + 1); while (first_pos <= last_block) { char *probe_blk = buffer; @@ -384,7 +400,12 @@ static void read_blocks(struct device *dev, if (next_pos > last_block) next_pos = last_block; - if (dev_read_blocks(dev, buffer, first_pos, next_pos)) + + eta_print(&bar); + int rc = dev_read_blocks(dev, buffer, first_pos, next_pos); + int errrc = errno; + eta_clear(); // as validate_block() might decide to print + if (errno = errrc, rc) warn("Failed to read blocks from 0x%" PRIx64 " to 0x%" PRIx64, first_pos, next_pos); @@ -396,6 +417,7 @@ static void read_blocks(struct device *dev, } first_pos = next_pos + 1; + eta_stamp(&bar, first_pos - first_block); } if (range.state != bs_unknown) print_block_range(&range); diff --git a/libeta.c b/libeta.c new file mode 100644 index 0000000..cc8a803 --- /dev/null +++ b/libeta.c @@ -0,0 +1,164 @@ +#include "libeta.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static uint64_t now_ms(void) +{ +#ifdef CLOCK_MONOTONIC + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + uint64_t ret = t.tv_sec; + ret *= 1000; + ret += t.tv_nsec / 1000000; +#else + struct timeval t; + gettimeofday(&t, NULL); + uint64_t ret = t.tv_sec; + ret *= 1000; + ret += t.tv_usec / 1000; +#endif + return ret; +} + +// _Generic is C11, the code is C99, so it's macro based dispatch. +#if INT64_MAX == INT_MAX +typedef div_t divi64_t; +divi64_t divi64(int64_t numer, int64_t denom) { return div(numer, denom); } +#elif INT64_MAX == LONG_MAX +typedef ldiv_t divi64_t; +divi64_t divi64(int64_t numer, int64_t denom) { return ldiv(numer, denom); } +#elif INT64_MAX == LLONG_MAX +typedef lldiv_t divi64_t; +divi64_t divi64(int64_t numer, int64_t denom) { return lldiv(numer, denom); } +#else +#error Unabled to dedice divi64() +#endif + +void eta_init(struct etabar *e, uint64_t plan) +{ + memset(e, 0, sizeof(*e)); + e->plan = plan; + e->start_ms = e->last_ms = now_ms(); +} + +// Exponential average of `speed` is an imprecise estimate as `speed` might +// fluctuate drastically. E.g. the flash drive that made me write this patch +// has a major (x40) speed dip on every 18th sample. Exponential average is +// way easier than an actual sliding window and it's good enough as the widget +// shows percentage and runtime as well. Improvement over average of `speed` +// might be average of `pace`. Time/work is meaningfully additive, while +// work/time is not. +// +// The damping factor might be calculated using following formula to remember +// _approximately_ N samples: alpha(N) = 1 - exp(-1 * log(2) / N) +void eta_stamp(struct etabar *e, uint64_t done) +{ + assert(e->done <= done && done <= e->plan); + const uint64_t now = now_ms(); + const uint64_t dt = now - e->last_ms; + const uint64_t dx = done - e->done; + if (!dt || !dx) // low timer resolution or no-op + return; + const double sample = ((double)dt) / dx; + const double alpha = 1. / 64; // 0.015625 ~ alpha(44)=0.01562 + const double beta = 1. - alpha; + e->pace = e->done ? (alpha * sample + beta * e->pace) : sample; + e->last_ms = now; + e->done = done; +} + +static const int64_t second_ms = 1000; +static const int64_t minute_ms = 60 * second_ms; +static const int64_t hour_ms = 60 * minute_ms; +static const int64_t day_ms = 24 * hour_ms; +static const int64_t day100_ms = 100 * day_ms; + +static int sprintf_us(char *s, uint64_t udt) +{ + int a, b; + int64_t dt = udt; + if (0 <= dt && dt < hour_ms) { + divi64_t q = divi64(dt, minute_ms); + a = q.quot; + b = q.rem / second_ms; + return snprintf(s, 7, "%02im%02is", a, b); + } else if (hour_ms <= dt && dt < day_ms) { + divi64_t q = divi64(dt, hour_ms); + a = q.quot; + b = q.rem / minute_ms; + return snprintf(s, 7, "%02ih%02im", a, b); + } else if (day_ms <= dt && dt < day100_ms) { + divi64_t q = divi64(dt, day_ms); + a = q.quot; + b = q.rem / hour_ms; + return snprintf(s, 7, "%02id%02ih", a, b); + } else { + // 100 days is basically eternity for a disk check run + return snprintf(s, 7, "(+inf)"); + } +} + +static const size_t widget_len = 23; // PP.P% NNhNNm/(+inf) [/] + +static const char spinchar[4] = {'-', '\\', '|', '/'}; +static const char spinbegin = '_'; +static const char spinend = '+'; +static unsigned spint = 0; + +static void mkwidget(char *widget, const struct etabar *e) +{ + char *it = widget; + assert(e->done <= e->plan); + const bool end = e->done == e->plan; + if (!end) + it += snprintf(it, 6, "%04.1f%%", floor(1000. * e->done / e->plan) / 10.); + else + it += snprintf(it, 6, " 100%%"); + *it++ = ' '; + it += sprintf_us(it, e->last_ms - e->start_ms); + *it++ = '/'; + if (e->pace != 0) + it += sprintf_us(it, (e->plan - e->done) * e->pace); + else + it += sprintf_us(it, INT64_MAX); + const char spinner = !e->done ? spinbegin : end ? spinend : spinchar[spint]; + spint = (spint + 1) % sizeof(spinchar); + it += snprintf(it, 5, " [%c]", spinner); + assert((size_t)(it - widget) == widget_len); +} + +int eta_print(struct etabar *e) +{ + char widget[widget_len + 1]; + mkwidget(widget, e); + fputs(widget, stdout); + return fflush(stdout); +} + +int eta_redraw(struct etabar *e) +{ + char widget[2 * widget_len + 1]; + memset(widget, '\b', widget_len); + mkwidget(widget + widget_len, e); + fputs(widget, stdout); + return fflush(stdout); +} + +int eta_clear(void) +{ + char widget[3 * widget_len + 1]; + memset(widget + 0 * widget_len, '\b', widget_len); + memset(widget + 1 * widget_len, ' ', widget_len); + memset(widget + 2 * widget_len, '\b', widget_len); + widget[3 * widget_len] = '\0'; + fputs(widget, stdout); + return fflush(stdout); +} diff --git a/libeta.h b/libeta.h new file mode 100644 index 0000000..c38df6a --- /dev/null +++ b/libeta.h @@ -0,0 +1,20 @@ +#ifndef HEADER_LIBETA_H +#define HEADER_LIBETA_H + +#include + +struct etabar { + uint64_t plan; + uint64_t done; + uint64_t start_ms; + uint64_t last_ms; + double pace; // milliseconds per unit-of-work +}; + +void eta_init(struct etabar *, uint64_t plan); +void eta_stamp(struct etabar *, uint64_t done); +int eta_print(struct etabar *); +int eta_redraw(struct etabar *); +int eta_clear(void); + +#endif From 4e979f68070d2ef5344b71b580521b830fbe9439 Mon Sep 17 00:00:00 2001 From: Leonid Evdokimov Date: Tue, 11 Mar 2025 15:13:47 +0300 Subject: [PATCH 2/2] gitignore: ignore the built binaries --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 202f62e..b2733f8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,9 @@ cscope.out *.o *~ -doc/_build \ No newline at end of file +doc/_build +/f3brew +/f3fix +/f3probe +/f3read +/f3write