From f3c949464abba68ebbb0a8ed0622275113707b17 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Thu, 26 Mar 2026 16:26:21 +0100 Subject: [PATCH 01/23] first test --- include/grass/defs/gis.h | 3 + include/grass/gis.h | 2 + lib/gis/percent.c | 480 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 484 insertions(+), 1 deletion(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index a3573b7fae5..907b43bd763 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -663,6 +663,9 @@ int G_stat(const char *, struct stat *); int G_owner(const char *); /* percent.c */ +GPercentContext *G_percent_context_create(size_t, size_t); +void G_percent_context_destroy(GPercentContext *); +void G_percent_r(GPercentContext *, size_t); void G_percent(long, long, int); void G_percent_reset(void); void G_progress(long, int); diff --git a/include/grass/gis.h b/include/grass/gis.h index 6dab2f835c8..bc769f94e11 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -727,6 +727,8 @@ struct ilist { int alloc_values; }; +typedef struct GPercentContext GPercentContext; + /*============================== Prototypes ================================*/ /* Since there are so many prototypes for the gis library they are stored */ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 9cb9c117032..f88d7964bb4 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -12,8 +12,486 @@ */ #include +#include +#include +#include +#include +#include +#include + #include +#define LOG_CAPACITY 1024 +#define LOG_MSG_SIZE 128 + +typedef enum { EV_LOG, EV_PROGRESS } event_type_t; + +typedef struct { + atomic_bool ready; + event_type_t type; + size_t completed; + size_t total; + char message[LOG_MSG_SIZE]; +} event_t; + +typedef struct { + event_t buffer[LOG_CAPACITY]; + atomic_size_t write_index; + size_t read_index; + atomic_size_t completed; + size_t total; + int info_format; + bool output_enabled; + atomic_long last_progress_ns; + long interval_ns; + size_t percent_step; + atomic_size_t next_percent_threshold; + atomic_bool stop; +} telemetry_t; + +struct GPercentContext { + telemetry_t telemetry; + atomic_bool initialized; + pthread_t consumer_thread; + atomic_bool consumer_started; +}; + +static telemetry_t g_percent_telemetry; +static atomic_bool g_percent_initialized = false; +static atomic_bool g_percent_consumer_started = false; + +static long now_ns(void); +static bool telemetry_has_pending_events(telemetry_t *); +static void telemetry_init(telemetry_t *, size_t, long); +static void telemetry_init_percent(telemetry_t *, size_t, size_t); +static void enqueue_event(telemetry_t *, event_t *); +static void telemetry_log(telemetry_t *, const char *); +static void telemetry_set_info_format(telemetry_t *t); +static void telemetry_set_output_enabled(telemetry_t *t); +static void telemetry_progress(telemetry_t *, size_t); +static void *telemetry_consumer(void *); +static void start_global_percent(size_t, size_t); + +/// Creates an isolated progress-reporting context for concurrent work. +/// +/// The returned context tracks progress for `total_num_elements` items and +/// emits progress updates whenever completion advances by at least +/// `percent_step` percentage points. If output is enabled by the current +/// runtime configuration, this function also starts the background consumer +/// thread used to flush queued telemetry events. +/// +/// - Parameter total_num_elements: Total number of elements to process. +/// - Parameter percent_step: Minimum percentage increment that triggers a +/// progress event. +/// - Returns: A newly allocated `GPercentContext`, or `NULL` if allocation +/// fails. +GPercentContext *G_percent_context_create(size_t total_num_elements, + size_t percent_step) +{ + GPercentContext *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + return NULL; + } + + atomic_init(&ctx->initialized, true); + telemetry_init_percent(&ctx->telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + ((percent_step > 0) ? percent_step : 0)); + atomic_init(&ctx->consumer_started, false); + + if (ctx->telemetry.output_enabled) { + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &ctx->consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, + &ctx->telemetry); + } + } + + return ctx; +} + +void G_percent_context_destroy(GPercentContext *ctx) +{ + if (!ctx) { + return; + } + + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { + free(ctx); + return; + } + + atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); + + if (ctx->telemetry.output_enabled && + atomic_exchange_explicit(&ctx->consumer_started, false, + memory_order_acq_rel)) { + pthread_join(ctx->consumer_thread, NULL); + } + + atomic_store_explicit(&ctx->initialized, false, memory_order_release); + free(ctx); +} + +void G_percent_r(GPercentContext *ctx, size_t current_element) +{ + if (!ctx) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + + telemetry_t *t = &ctx->telemetry; + if (t->total == 0 || t->percent_step == 0 || !t->output_enabled) + return; + + size_t total = t->total; + size_t completed = (current_element < 0) ? 0 : current_element; + if (completed > total) + completed = total; + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = + atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(t, &ev); + return; + } + } +} + +void G_percent(long current_element, long total_num_elements, int percent_step) +{ + if (total_num_elements <= 0) + return; + + start_global_percent((size_t)total_num_elements, (size_t)percent_step); + + // If someone initialized with different totals/steps, we keep the first + // ones for simplicity. + + size_t total = (size_t)total_num_elements; + size_t completed = (current_element < 0) ? 0 : (size_t)current_element; + if (completed > total) + completed = total; + + if (g_percent_telemetry.percent_step == 0 || + !g_percent_telemetry.output_enabled) + return; // not configured + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = atomic_load_explicit( + &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + g_percent_telemetry.percent_step; + if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &g_percent_telemetry.next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(&g_percent_telemetry, &ev); + if (completed == total) { + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + } + return; + } + // CAS failed; expected updated, loop continues + } +} + +static void *telemetry_consumer(void *arg) +{ + telemetry_t *t = arg; + + while (true) { + if (atomic_load_explicit(&t->stop, memory_order_acquire) && + !telemetry_has_pending_events(t)) { + break; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + + if (!atomic_load_explicit(&ev->ready, memory_order_acquire)) { + sched_yield(); + continue; + } + + // handle event + if (ev->type == EV_LOG) { + printf("[LOG] %s\n", ev->message); + } + else if (ev->type == EV_PROGRESS) { + double pct = (ev->total > 0) + ? (double)ev->completed * 100.0 / (double)ev->total + : 0.0; + + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); + break; + case G_INFO_FORMAT_GUI: + fprintf(stderr, "GRASS_INFO_PERCENT: %d", (int)pct); + fflush(stderr); + break; + case G_INFO_FORMAT_PLAIN: + fprintf(stderr, "%d%s", (int)pct, + ((int)pct == 100 ? "" : "..")); + break; + default: + break; + } + } + + // mark slot free + atomic_store_explicit(&ev->ready, false, memory_order_release); + t->read_index++; + } + + if (t == &g_percent_telemetry) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + } + + return NULL; +} + +static void telemetry_init(telemetry_t *t, size_t total, long interval_ms) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + telemetry_set_output_enabled(t); + + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = interval_ms * 1000000L; + + t->percent_step = 0; // 0 => disabled, use time-based if interval_ns > 0 + atomic_init(&t->next_percent_threshold, 0); + + atomic_init(&t->stop, false); +} + +static void telemetry_init_percent(telemetry_t *t, size_t total, + size_t percent_step) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + telemetry_set_output_enabled(t); + + // disable time-based gating + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = 0; + + // enable percentage-based gating + t->percent_step = percent_step; + size_t first = percent_step > 0 ? percent_step : 0; + atomic_init(&t->next_percent_threshold, first); + + atomic_init(&t->stop, false); +} + +static void enqueue_event(telemetry_t *t, event_t *src) +{ + size_t idx = + atomic_fetch_add_explicit(&t->write_index, 1, memory_order_relaxed); + + event_t *dst = &t->buffer[idx % LOG_CAPACITY]; + + // wait until slot is free (bounded spin) + while (atomic_load_explicit(&dst->ready, memory_order_acquire)) { + sched_yield(); + } + + // copy payload + *dst = *src; + + // publish + atomic_store_explicit(&dst->ready, true, memory_order_release); +} + +static bool telemetry_has_pending_events(telemetry_t *t) +{ + if (t->read_index != + atomic_load_explicit(&t->write_index, memory_order_acquire)) { + return true; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + return atomic_load_explicit(&ev->ready, memory_order_acquire); +} + +static void telemetry_log(telemetry_t *t, const char *msg) +{ + event_t ev = {0}; + ev.type = EV_LOG; + snprintf(ev.message, LOG_MSG_SIZE, "%s", msg); + + enqueue_event(t, &ev); +} + +static void telemetry_set_info_format(telemetry_t *t) +{ + t->info_format = G_info_format(); +} + +static void telemetry_set_output_enabled(telemetry_t *t) +{ + t->output_enabled = + t->info_format != G_INFO_FORMAT_SILENT && G_verbose() >= 1; +} + +/// Records completed work and enqueues a progress event when the next +/// reportable threshold is reached. +/// +/// The function atomically increments the telemetry's completed counter by +/// `step`, then decides whether to emit a progress event using one of two +/// modes: percent-based reporting when `percent_step` and `total` are +/// configured, or time-based throttling when they are not. Atomic +/// compare-and-swap operations ensure that only one caller emits an event for a +/// given threshold or interval. +/// +/// - Parameter t: The telemetry state to update and publish through. +/// - Parameter step: The number of newly completed units of work to add. +static void telemetry_progress(telemetry_t *t, size_t step) +{ + size_t new_completed = + atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + + step; + + if (t->percent_step > 0 && t->total > 0) { + size_t current_pct = (size_t)((new_completed * 100) / t->total); + size_t expected = atomic_load_explicit(&t->next_percent_threshold, + memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (next > 100) + next = 101; // sentinel beyond 100 to stop further emits + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + // we won the right to emit at this threshold + break; + } + // CAS failed, expected now contains the latest value; loop to + // re-check + } + // If we didn't advance, nothing to emit + if (current_pct < expected || expected > 100) { + return; + } + } + else { + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit( + &t->last_progress_ns, &last, now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = new_completed; + ev.total = t->total; + + enqueue_event(t, &ev); +} + +/// Initializes shared percent-based telemetry and starts the detached consumer +/// thread once. +/// +/// This function performs one-time global setup for percent progress reporting. +/// Repeated calls return immediately after the initialization state has been +/// set. If output is disabled or the consumer thread cannot be created, no +/// further progress consumer setup is performed. +/// +/// - Parameter total_num_elements: The total number of elements used to compute +/// progress percentages. +/// - Parameter percent_step: The percentage increment that controls when +/// progress updates are emitted. +static void start_global_percent(size_t total_num_elements, size_t percent_step) +{ + bool expected_init = false; + if (!atomic_compare_exchange_strong_explicit( + &g_percent_initialized, &expected_init, true, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + telemetry_init_percent(&g_percent_telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + ((percent_step > 0) ? percent_step : 0)); + + if (!g_percent_telemetry.output_enabled) { + return; + } + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &g_percent_consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_t consumer_thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&consumer_thread, &attr, telemetry_consumer, + &g_percent_telemetry) != 0) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + } + pthread_attr_destroy(&attr); + } +} + +/// Returns the current UTC time in nanoseconds. +static long now_ns(void) +{ + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +} + +// ---------------------------------------------------------------------------- + static struct state { int prev; int first; @@ -58,7 +536,7 @@ static int (*ext_percent)(int); \param d total number of elements \param s increment size */ -void G_percent(long n, long d, int s) +void G_percent_old(long n, long d, int s) { int x, format; From eecb86dac5e8853ad234158cbe921343fb0d3f17 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Fri, 27 Mar 2026 08:45:46 +0100 Subject: [PATCH 02/23] add G_PERCENT_INCR_TYPE enum and some docs --- include/grass/defs/gis.h | 3 +- include/grass/gis.h | 5 ++ lib/gis/percent.c | 186 ++++++++++++++++++++++++++++----------- 3 files changed, 144 insertions(+), 50 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index 907b43bd763..546e4dce47c 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -663,10 +663,11 @@ int G_stat(const char *, struct stat *); int G_owner(const char *); /* percent.c */ -GPercentContext *G_percent_context_create(size_t, size_t); +GPercentContext *G_percent_context_create(size_t, size_t, G_PERCENT_INCR_TYPE); void G_percent_context_destroy(GPercentContext *); void G_percent_r(GPercentContext *, size_t); void G_percent(long, long, int); +void G_percent_old(long, long, int); void G_percent_reset(void); void G_progress(long, int); void G_set_percent_routine(int (*)(int)); diff --git a/include/grass/gis.h b/include/grass/gis.h index bc769f94e11..ff1f5e3f11c 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -729,6 +729,11 @@ struct ilist { typedef struct GPercentContext GPercentContext; +typedef enum { + G_PERCENT_INCR_STEP, + G_PERCENT_INCR_TIME, +} G_PERCENT_INCR_TYPE; + /*============================== Prototypes ================================*/ /* Since there are so many prototypes for the gis library they are stored */ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index f88d7964bb4..2a0f9205b00 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -10,7 +10,7 @@ \author GRASS Development Team */ - +#include #include #include #include @@ -41,7 +41,6 @@ typedef struct { atomic_size_t completed; size_t total; int info_format; - bool output_enabled; atomic_long last_progress_ns; long interval_ns; size_t percent_step; @@ -60,17 +59,17 @@ static telemetry_t g_percent_telemetry; static atomic_bool g_percent_initialized = false; static atomic_bool g_percent_consumer_started = false; -static long now_ns(void); static bool telemetry_has_pending_events(telemetry_t *); static void telemetry_init(telemetry_t *, size_t, long); static void telemetry_init_percent(telemetry_t *, size_t, size_t); static void enqueue_event(telemetry_t *, event_t *); static void telemetry_log(telemetry_t *, const char *); -static void telemetry_set_info_format(telemetry_t *t); -static void telemetry_set_output_enabled(telemetry_t *t); +static void telemetry_set_info_format(telemetry_t *); static void telemetry_progress(telemetry_t *, size_t); static void *telemetry_consumer(void *); static void start_global_percent(size_t, size_t); +static bool output_is_silenced(void); +static long now_ns(void); /// Creates an isolated progress-reporting context for concurrent work. /// @@ -80,38 +79,61 @@ static void start_global_percent(size_t, size_t); /// runtime configuration, this function also starts the background consumer /// thread used to flush queued telemetry events. /// -/// - Parameter total_num_elements: Total number of elements to process. -/// - Parameter percent_step: Minimum percentage increment that triggers a +/// \param total_num_elements Total number of elements to process. +/// \param step Minimum percentage increment that triggers a /// progress event. -/// - Returns: A newly allocated `GPercentContext`, or `NULL` if allocation -/// fails. +/// \return A newly allocated `GPercentContext`, or `NULL` if output +/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or +/// verbosity level is below `1`. GPercentContext *G_percent_context_create(size_t total_num_elements, - size_t percent_step) + size_t step, + G_PERCENT_INCR_TYPE incr_type) { - GPercentContext *ctx = calloc(1, sizeof(*ctx)); - if (!ctx) { - return NULL; - } + if (output_is_silenced()) + return; + + GPercentContext *ctx = G_calloc(1, sizeof(*ctx)); atomic_init(&ctx->initialized, true); - telemetry_init_percent(&ctx->telemetry, - ((total_num_elements > 0) ? total_num_elements : 0), - ((percent_step > 0) ? percent_step : 0)); + + switch (incr_type) { + case G_PERCENT_INCR_STEP: + assert(step <= 100 && step > 0); + telemetry_init_percent( + &ctx->telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + ((step > 0) ? step : 0)); + break; + case G_PERCENT_INCR_TIME: + assert(step >= 0); + telemetry_init(&ctx->telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + (long)((step > 0) ? step : 0)); + default: + break; + } atomic_init(&ctx->consumer_started, false); - if (ctx->telemetry.output_enabled) { - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &ctx->consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, - &ctx->telemetry); - } + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &ctx->consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, + &ctx->telemetry); } return ctx; } +/// Destroys a `GPercentContext` and releases any resources it owns. +/// +/// This function stops the context's background telemetry consumer, waits for +/// the consumer thread to finish when it was started, marks the context as no +/// longer initialized, and frees the context memory. Passing `NULL` is safe and +/// has no effect. +/// +/// \param ctx The progress-reporting context previously created by +/// `G_percent_context_create()`, or `NULL`. void G_percent_context_destroy(GPercentContext *ctx) { if (!ctx) { @@ -125,8 +147,7 @@ void G_percent_context_destroy(GPercentContext *ctx) atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); - if (ctx->telemetry.output_enabled && - atomic_exchange_explicit(&ctx->consumer_started, false, + if (atomic_exchange_explicit(&ctx->consumer_started, false, memory_order_acq_rel)) { pthread_join(ctx->consumer_thread, NULL); } @@ -135,6 +156,21 @@ void G_percent_context_destroy(GPercentContext *ctx) free(ctx); } +/// Reports progress for an isolated `GPercentContext` instance. +/// +/// This re-entrant variant of `G_percent` is intended for concurrent or +/// context-specific work. It validates that `ctx` is initialized, clamps +/// `current_element` to the valid `0...total` range, and enqueues a progress +/// event only when the computed percentage reaches the next configured +/// threshold for the context. +/// +/// Callers typically create the context with `G_percent_context_create()`, call +/// this function as work advances, and later release resources with +/// `G_percent_context_destroy()`. +/// +/// \param ctx The progress-reporting context created by +/// `G_percent_context_create()`. +/// \param current_element: The current completed element index or count. void G_percent_r(GPercentContext *ctx, size_t current_element) { if (!ctx) @@ -143,7 +179,7 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) return; telemetry_t *t = &ctx->telemetry; - if (t->total == 0 || t->percent_step == 0 || !t->output_enabled) + if (t->total == 0 || t->percent_step == 0) return; size_t total = t->total; @@ -171,9 +207,26 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) } } +/// Reports global progress when completion crosses the next percentage step. +/// +/// This function initializes the shared global telemetry stream on first use, +/// clamps `current_element` into the valid `0...total_num_elements` range, and +/// enqueues a progress update only when the computed percentage reaches the +/// next configured threshold. When progress reaches the total, the background +/// consumer is asked to stop after pending events have been flushed. +/// +/// Callers typically invoke this before the expensive unit of work in a loop +/// and then make a final call such as `G_percent(1, 1, 1)` to ensure `100%` is +/// emitted. +/// +/// \param current_element The current completed element index or count. +/// \param total_num_elements The total number of elements to process. Values +/// less than or equal to `0` disable reporting. +/// \param percent_step The minimum percentage increment required before a new +/// progress event is emitted. void G_percent(long current_element, long total_num_elements, int percent_step) { - if (total_num_elements <= 0) + if (total_num_elements <= 0 || output_is_silenced()) return; start_global_percent((size_t)total_num_elements, (size_t)percent_step); @@ -186,8 +239,7 @@ void G_percent(long current_element, long total_num_elements, int percent_step) if (completed > total) completed = total; - if (g_percent_telemetry.percent_step == 0 || - !g_percent_telemetry.output_enabled) + if (g_percent_telemetry.percent_step == 0) return; // not configured size_t current_pct = (size_t)((completed * 100) / total); @@ -215,6 +267,13 @@ void G_percent(long current_element, long total_num_elements, int percent_step) } } +/// Consumes queued telemetry events and emits log or progress output until +/// shutdown is requested and the event buffer has been drained. +/// +/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and +/// formatting settings should be consumed. +/// \return `NULL` after the consumer loop exits and any global consumer state +/// has been reset. static void *telemetry_consumer(void *arg) { telemetry_t *t = arg; @@ -285,7 +344,6 @@ static void telemetry_init(telemetry_t *t, size_t total, long interval_ms) atomic_init(&t->completed, 0); t->total = total; telemetry_set_info_format(t); - telemetry_set_output_enabled(t); atomic_init(&t->last_progress_ns, 0); t->interval_ns = interval_ms * 1000000L; @@ -296,6 +354,18 @@ static void telemetry_init(telemetry_t *t, size_t total, long interval_ms) atomic_init(&t->stop, false); } +/// Initializes telemetry state for percentage-based progress reporting. +/// +/// Resets the telemetry ring buffer and counters, disables time-based +/// throttling, and configures the next progress event to be emitted when the +/// completed work reaches the first `percent_step` threshold. +/// +/// \param t The telemetry instance to reset and configure. +/// \param total The total number of work units expected for the tracked +/// operation. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. A value of `0` disables percentage-based +/// thresholds. static void telemetry_init_percent(telemetry_t *t, size_t total, size_t percent_step) { @@ -307,7 +377,6 @@ static void telemetry_init_percent(telemetry_t *t, size_t total, atomic_init(&t->completed, 0); t->total = total; telemetry_set_info_format(t); - telemetry_set_output_enabled(t); // disable time-based gating atomic_init(&t->last_progress_ns, 0); @@ -321,6 +390,14 @@ static void telemetry_init_percent(telemetry_t *t, size_t total, atomic_init(&t->stop, false); } +/// Queues a telemetry event into the ring buffer for later consumption. +/// +/// Waits until the destination slot becomes available, copies the event payload +/// into that slot, and then marks the slot as ready using release semantics so +/// readers can safely observe the published event. +/// +/// \param t The telemetry instance that owns the event buffer. +/// \param src The event payload to enqueue. static void enqueue_event(telemetry_t *t, event_t *src) { size_t idx = @@ -340,6 +417,16 @@ static void enqueue_event(telemetry_t *t, event_t *src) atomic_store_explicit(&dst->ready, true, memory_order_release); } +/// Determines whether the telemetry ring buffer still contains unread events. +/// +/// Checks for pending work by first comparing the consumer read position with +/// the producer write position, then verifying whether the current buffer slot +/// has been published and marked ready. Acquire loads ensure the caller +/// observes event availability consistently across threads. +/// +/// \param t The telemetry instance whose event queue is being inspected. +/// \return `true` when at least one event is available to consume; otherwise +/// `false`. static bool telemetry_has_pending_events(telemetry_t *t) { if (t->read_index != @@ -360,17 +447,17 @@ static void telemetry_log(telemetry_t *t, const char *msg) enqueue_event(t, &ev); } +/// Captures the current GRASS info output format for subsequent telemetry. +/// +/// Reads the process-wide info formatting mode and stores it on the telemetry +/// instance so later progress and log events can format output consistently. +/// +/// \param t The telemetry state that caches the active info format. static void telemetry_set_info_format(telemetry_t *t) { t->info_format = G_info_format(); } -static void telemetry_set_output_enabled(telemetry_t *t) -{ - t->output_enabled = - t->info_format != G_INFO_FORMAT_SILENT && G_verbose() >= 1; -} - /// Records completed work and enqueues a progress event when the next /// reportable threshold is reached. /// @@ -381,8 +468,8 @@ static void telemetry_set_output_enabled(telemetry_t *t) /// compare-and-swap operations ensure that only one caller emits an event for a /// given threshold or interval. /// -/// - Parameter t: The telemetry state to update and publish through. -/// - Parameter step: The number of newly completed units of work to add. +/// \param t The telemetry state to update and publish through. +/// \param step The number of newly completed units of work to add. static void telemetry_progress(telemetry_t *t, size_t step) { size_t new_completed = @@ -441,10 +528,10 @@ static void telemetry_progress(telemetry_t *t, size_t step) /// set. If output is disabled or the consumer thread cannot be created, no /// further progress consumer setup is performed. /// -/// - Parameter total_num_elements: The total number of elements used to compute -/// progress percentages. -/// - Parameter percent_step: The percentage increment that controls when -/// progress updates are emitted. +/// \param total_num_elements The total number of elements used to compute +/// progress percentages. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. static void start_global_percent(size_t total_num_elements, size_t percent_step) { bool expected_init = false; @@ -458,10 +545,6 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) ((total_num_elements > 0) ? total_num_elements : 0), ((percent_step > 0) ? percent_step : 0)); - if (!g_percent_telemetry.output_enabled) { - return; - } - bool expected_started = false; if (atomic_compare_exchange_strong_explicit( &g_percent_consumer_started, &expected_started, true, @@ -482,6 +565,11 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) } } +static bool output_is_silenced(void) +{ + return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); +} + /// Returns the current UTC time in nanoseconds. static long now_ns(void) { From 562203c4a2b6a234bda626644cd955a2cbf0c738 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Fri, 27 Mar 2026 11:00:25 +0100 Subject: [PATCH 03/23] new G_percent_context_create_time and related updates --- include/grass/defs/gis.h | 3 ++- include/grass/gis.h | 5 ---- lib/gis/percent.c | 57 ++++++++++++++++++++++------------------ 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index 546e4dce47c..06a712589de 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -663,7 +663,8 @@ int G_stat(const char *, struct stat *); int G_owner(const char *); /* percent.c */ -GPercentContext *G_percent_context_create(size_t, size_t, G_PERCENT_INCR_TYPE); +GPercentContext *G_percent_context_create(size_t, size_t); +GPercentContext *G_percent_context_create_time(size_t, long); void G_percent_context_destroy(GPercentContext *); void G_percent_r(GPercentContext *, size_t); void G_percent(long, long, int); diff --git a/include/grass/gis.h b/include/grass/gis.h index ff1f5e3f11c..bc769f94e11 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -729,11 +729,6 @@ struct ilist { typedef struct GPercentContext GPercentContext; -typedef enum { - G_PERCENT_INCR_STEP, - G_PERCENT_INCR_TIME, -} G_PERCENT_INCR_TYPE; - /*============================== Prototypes ================================*/ /* Since there are so many prototypes for the gis library they are stored */ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 2a0f9205b00..f9287a9dfd0 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -21,8 +21,9 @@ #include -#define LOG_CAPACITY 1024 -#define LOG_MSG_SIZE 128 +#define LOG_CAPACITY 1024 +#define LOG_MSG_SIZE 128 +#define TIME_RATE_LIMIT_MS 100 typedef enum { EV_LOG, EV_PROGRESS } event_type_t; @@ -59,8 +60,9 @@ static telemetry_t g_percent_telemetry; static atomic_bool g_percent_initialized = false; static atomic_bool g_percent_consumer_started = false; +static GPercentContext *context_create(size_t, size_t, long); static bool telemetry_has_pending_events(telemetry_t *); -static void telemetry_init(telemetry_t *, size_t, long); +static void telemetry_init_time(telemetry_t *, size_t, long); static void telemetry_init_percent(telemetry_t *, size_t, size_t); static void enqueue_event(telemetry_t *, event_t *); static void telemetry_log(telemetry_t *, const char *); @@ -86,31 +88,36 @@ static long now_ns(void); /// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or /// verbosity level is below `1`. GPercentContext *G_percent_context_create(size_t total_num_elements, - size_t step, - G_PERCENT_INCR_TYPE incr_type) + size_t step) +{ + return context_create(total_num_elements, step, + (step == 0 ? TIME_RATE_LIMIT_MS : 0)); +} + +GPercentContext *G_percent_context_create_time(size_t total_num_elements, + long interval_ms) +{ + return context_create(total_num_elements, 0, interval_ms); +} + +static GPercentContext *context_create(size_t total_num_elements, size_t step, + long interval_ms) { if (output_is_silenced()) - return; + return NULL; GPercentContext *ctx = G_calloc(1, sizeof(*ctx)); atomic_init(&ctx->initialized, true); - switch (incr_type) { - case G_PERCENT_INCR_STEP: - assert(step <= 100 && step > 0); - telemetry_init_percent( - &ctx->telemetry, - ((total_num_elements > 0) ? total_num_elements : 0), - ((step > 0) ? step : 0)); - break; - case G_PERCENT_INCR_TIME: - assert(step >= 0); - telemetry_init(&ctx->telemetry, - ((total_num_elements > 0) ? total_num_elements : 0), - (long)((step > 0) ? step : 0)); - default: - break; + assert(step <= 100); + + if (step == 0) { + assert(interval_ms > 0); + telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); + } + else { + telemetry_init_percent(&ctx->telemetry, total_num_elements, step); } atomic_init(&ctx->consumer_started, false); @@ -141,7 +148,7 @@ void G_percent_context_destroy(GPercentContext *ctx) } if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { - free(ctx); + G_free(ctx); return; } @@ -153,7 +160,7 @@ void G_percent_context_destroy(GPercentContext *ctx) } atomic_store_explicit(&ctx->initialized, false, memory_order_release); - free(ctx); + G_free(ctx); } /// Reports progress for an isolated `GPercentContext` instance. @@ -179,7 +186,7 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) return; telemetry_t *t = &ctx->telemetry; - if (t->total == 0 || t->percent_step == 0) + if (t->total == 0 || (t->percent_step == 0 && t->interval_ns == 0)) return; size_t total = t->total; @@ -332,7 +339,7 @@ static void *telemetry_consumer(void *arg) return NULL; } -static void telemetry_init(telemetry_t *t, size_t total, long interval_ms) +static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) { atomic_init(&t->write_index, 0); t->read_index = 0; From 0900e6cb80e81d7911738d2113de44d80b0f9e43 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Fri, 27 Mar 2026 11:49:26 +0100 Subject: [PATCH 04/23] split init logic for time and percentage --- lib/gis/percent.c | 37 ++++++++++++++++++++++++++++++++++++- raster/r.texture/execute.c | 5 +++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index f9287a9dfd0..aea0f1dfb8e 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -49,8 +49,11 @@ typedef struct { atomic_bool stop; } telemetry_t; +typedef void (*context_progress_fn)(telemetry_t *, size_t); + struct GPercentContext { telemetry_t telemetry; + context_progress_fn report_progress; atomic_bool initialized; pthread_t consumer_thread; atomic_bool consumer_started; @@ -68,6 +71,8 @@ static void enqueue_event(telemetry_t *, event_t *); static void telemetry_log(telemetry_t *, const char *); static void telemetry_set_info_format(telemetry_t *); static void telemetry_progress(telemetry_t *, size_t); +static void context_progress_percent(telemetry_t *, size_t); +static void context_progress_time(telemetry_t *, size_t); static void *telemetry_consumer(void *); static void start_global_percent(size_t, size_t); static bool output_is_silenced(void); @@ -115,9 +120,11 @@ static GPercentContext *context_create(size_t total_num_elements, size_t step, if (step == 0) { assert(interval_ms > 0); telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); + ctx->report_progress = context_progress_time; } else { telemetry_init_percent(&ctx->telemetry, total_num_elements, step); + ctx->report_progress = context_progress_percent; } atomic_init(&ctx->consumer_started, false); @@ -186,7 +193,7 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) return; telemetry_t *t = &ctx->telemetry; - if (t->total == 0 || (t->percent_step == 0 && t->interval_ns == 0)) + if (t->total == 0) return; size_t total = t->total; @@ -194,6 +201,12 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) if (completed > total) completed = total; + ctx->report_progress(t, completed); +} + +static void context_progress_percent(telemetry_t *t, size_t completed) +{ + size_t total = t->total; size_t current_pct = (size_t)((completed * 100) / total); size_t expected = atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); @@ -214,6 +227,28 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) } } +static void context_progress_time(telemetry_t *t, size_t completed) +{ + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, + now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = t->total; + enqueue_event(t, &ev); +} + /// Reports global progress when completion crosses the next percentage step. /// /// This function initializes the shared global telemetry stream on first use, diff --git a/raster/r.texture/execute.c b/raster/r.texture/execute.c index 80514e6af86..f2234454d73 100644 --- a/raster/r.texture/execute.c +++ b/raster/r.texture/execute.c @@ -104,12 +104,13 @@ int execute_texture(CELL **data, struct dimensions *dim, else G_message(_("Calculating %s..."), measure_menu[measure_idx[0]].desc); + GPercentContext *ctx = G_percent_context_create_time(nrows, 500); #pragma omp parallel private(row, col, i, j, measure, trow) default(shared) { #pragma omp for schedule(static, 1) ordered for (row = first_row; row < last_row; row++) { trow = row % threads; /* Obtain thread row id */ - G_percent(row, nrows, 2); + G_percent_r(ctx, row); /* initialize the output row */ for (i = 0; i < n_outputs; i++) @@ -164,7 +165,7 @@ int execute_texture(CELL **data, struct dimensions *dim, Rast_put_row(outfd[i], fbuf_threads[0][0], out_data_type); } } - G_percent(nrows, nrows, 1); + G_percent_context_destroy(ctx); for (i = 0; i < threads; i++) { for (j = 0; j < n_outputs; j++) From 985769fe10f484d8013980e8ee1e73171f4a3609 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Fri, 27 Mar 2026 12:44:50 +0100 Subject: [PATCH 05/23] reorganise --- lib/gis/percent.c | 158 +++++++++++++++++++++++----------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index aea0f1dfb8e..03c816cee10 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -105,40 +105,6 @@ GPercentContext *G_percent_context_create_time(size_t total_num_elements, return context_create(total_num_elements, 0, interval_ms); } -static GPercentContext *context_create(size_t total_num_elements, size_t step, - long interval_ms) -{ - if (output_is_silenced()) - return NULL; - - GPercentContext *ctx = G_calloc(1, sizeof(*ctx)); - - atomic_init(&ctx->initialized, true); - - assert(step <= 100); - - if (step == 0) { - assert(interval_ms > 0); - telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); - ctx->report_progress = context_progress_time; - } - else { - telemetry_init_percent(&ctx->telemetry, total_num_elements, step); - ctx->report_progress = context_progress_percent; - } - atomic_init(&ctx->consumer_started, false); - - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &ctx->consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, - &ctx->telemetry); - } - - return ctx; -} - /// Destroys a `GPercentContext` and releases any resources it owns. /// /// This function stops the context's background telemetry consumer, waits for @@ -204,51 +170,6 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) ctx->report_progress(t, completed); } -static void context_progress_percent(telemetry_t *t, size_t completed) -{ - size_t total = t->total; - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = - atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + t->percent_step; - if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &t->next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(t, &ev); - return; - } - } -} - -static void context_progress_time(telemetry_t *t, size_t completed) -{ - long now = now_ns(); - long last = - atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); - - if (now - last < t->interval_ns) { - return; - } - if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, - now, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = t->total; - enqueue_event(t, &ev); -} - /// Reports global progress when completion crosses the next percentage step. /// /// This function initializes the shared global telemetry stream on first use, @@ -309,6 +230,85 @@ void G_percent(long current_element, long total_num_elements, int percent_step) } } +static GPercentContext *context_create(size_t total_num_elements, size_t step, + long interval_ms) +{ + if (output_is_silenced()) + return NULL; + + GPercentContext *ctx = G_calloc(1, sizeof(*ctx)); + + atomic_init(&ctx->initialized, true); + + assert(step <= 100); + + if (step == 0) { + assert(interval_ms > 0); + telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); + ctx->report_progress = context_progress_time; + } + else { + telemetry_init_percent(&ctx->telemetry, total_num_elements, step); + ctx->report_progress = context_progress_percent; + } + atomic_init(&ctx->consumer_started, false); + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &ctx->consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, + &ctx->telemetry); + } + + return ctx; +} + +static void context_progress_percent(telemetry_t *t, size_t completed) +{ + size_t total = t->total; + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = + atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(t, &ev); + return; + } + } +} + +static void context_progress_time(telemetry_t *t, size_t completed) +{ + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, + now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = t->total; + enqueue_event(t, &ev); +} + /// Consumes queued telemetry events and emits log or progress output until /// shutdown is requested and the event buffer has been drained. /// From 646968a070d477605b5002e18effb2b036ef55bc Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Sat, 28 Mar 2026 18:05:25 +0100 Subject: [PATCH 06/23] last 100 per cent --- lib/gis/percent.c | 99 ++++++++++++++++++++++++++++--------- raster/r.buffer/write_map.c | 6 ++- raster/r.texture/execute.c | 6 ++- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 03c816cee10..84bf5a75fc1 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -68,6 +68,7 @@ static bool telemetry_has_pending_events(telemetry_t *); static void telemetry_init_time(telemetry_t *, size_t, long); static void telemetry_init_percent(telemetry_t *, size_t, size_t); static void enqueue_event(telemetry_t *, event_t *); +static void telemetry_enqueue_final_progress(telemetry_t *); static void telemetry_log(telemetry_t *, const char *); static void telemetry_set_info_format(telemetry_t *); static void telemetry_progress(telemetry_t *, size_t); @@ -82,9 +83,13 @@ static long now_ns(void); /// /// The returned context tracks progress for `total_num_elements` items and /// emits progress updates whenever completion advances by at least -/// `percent_step` percentage points. If output is enabled by the current -/// runtime configuration, this function also starts the background consumer -/// thread used to flush queued telemetry events. +/// `percent_step` percentage points. `total_num_elements` must match the +/// actual number of work units that will be reported through `G_percent_r()`. +/// In particular, callers should pass a completed-work count, not a raw loop +/// index or a larger container size, otherwise the terminal `100%` update may +/// never be reached. If output is enabled by the current runtime +/// configuration, this function also starts the background consumer thread +/// used to flush queued telemetry events. /// /// \param total_num_elements Total number of elements to process. /// \param step Minimum percentage increment that triggers a @@ -125,6 +130,13 @@ void G_percent_context_destroy(GPercentContext *ctx) return; } + if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= + ctx->telemetry.total && + atomic_load_explicit(&ctx->telemetry.next_percent_threshold, + memory_order_acquire) <= 100) { + telemetry_enqueue_final_progress(&ctx->telemetry); + } + atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); if (atomic_exchange_explicit(&ctx->consumer_started, false, @@ -167,6 +179,7 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) if (completed > total) completed = total; + atomic_store_explicit(&t->completed, completed, memory_order_release); ctx->report_progress(t, completed); } @@ -175,12 +188,9 @@ void G_percent_r(GPercentContext *ctx, size_t current_element) /// This function initializes the shared global telemetry stream on first use, /// clamps `current_element` into the valid `0...total_num_elements` range, and /// enqueues a progress update only when the computed percentage reaches the -/// next configured threshold. When progress reaches the total, the background -/// consumer is asked to stop after pending events have been flushed. -/// -/// Callers typically invoke this before the expensive unit of work in a loop -/// and then make a final call such as `G_percent(1, 1, 1)` to ensure `100%` is -/// emitted. +/// next configured threshold. When progress reaches the total, a terminal +/// `100%` event is always queued and the background consumer is asked to stop +/// after pending events have been flushed. /// /// \param current_element The current completed element index or count. /// \param total_num_elements The total number of elements to process. Values @@ -205,12 +215,24 @@ void G_percent(long current_element, long total_num_elements, int percent_step) if (g_percent_telemetry.percent_step == 0) return; // not configured + atomic_store_explicit(&g_percent_telemetry.completed, completed, + memory_order_release); + + if (completed == total) { + telemetry_enqueue_final_progress(&g_percent_telemetry); + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + return; + } + size_t current_pct = (size_t)((completed * 100) / total); size_t expected = atomic_load_explicit( &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); while (current_pct >= expected && expected <= 100) { size_t next = expected + g_percent_telemetry.percent_step; - if (next > 100) + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) next = 101; if (atomic_compare_exchange_strong_explicit( &g_percent_telemetry.next_percent_threshold, &expected, next, @@ -267,12 +289,20 @@ static GPercentContext *context_create(size_t total_num_elements, size_t step, static void context_progress_percent(telemetry_t *t, size_t completed) { size_t total = t->total; + + if (completed == total) { + telemetry_enqueue_final_progress(t); + return; + } + size_t current_pct = (size_t)((completed * 100) / total); size_t expected = atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); while (current_pct >= expected && expected <= 100) { size_t next = expected + t->percent_step; - if (next > 100) + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) next = 101; if (atomic_compare_exchange_strong_explicit( &t->next_percent_threshold, &expected, next, @@ -289,6 +319,11 @@ static void context_progress_percent(telemetry_t *t, size_t completed) static void context_progress_time(telemetry_t *t, size_t completed) { + if (completed == t->total) { + telemetry_enqueue_final_progress(t); + return; + } + long now = now_ns(); long last = atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); @@ -345,14 +380,16 @@ static void *telemetry_consumer(void *arg) switch (t->info_format) { case G_INFO_FORMAT_STANDARD: fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); + if ((int)pct == 100) + fprintf(stderr, "\n"); break; case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PERCENT: %d", (int)pct); + fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); fflush(stderr); break; case G_INFO_FORMAT_PLAIN: fprintf(stderr, "%d%s", (int)pct, - ((int)pct == 100 ? "" : "..")); + ((int)pct == 100 ? "\n" : "..")); break; default: break; @@ -459,16 +496,27 @@ static void enqueue_event(telemetry_t *t, event_t *src) atomic_store_explicit(&dst->ready, true, memory_order_release); } -/// Determines whether the telemetry ring buffer still contains unread events. +/// Queues a terminal `100%` progress event for a telemetry stream. /// -/// Checks for pending work by first comparing the consumer read position with -/// the producer write position, then verifying whether the current buffer slot -/// has been published and marked ready. Acquire loads ensure the caller -/// observes event availability consistently across threads. +/// This helper records the stream as fully completed, disables further +/// percentage-threshold reporting, and enqueues one last progress event with +/// `completed == total` so the consumer can emit the final `100%` update. /// -/// \param t The telemetry instance whose event queue is being inspected. -/// \return `true` when at least one event is available to consume; otherwise -/// `false`. +/// \param t The telemetry instance to finalize. +static void telemetry_enqueue_final_progress(telemetry_t *t) +{ + event_t ev = {0}; + + atomic_store_explicit(&t->completed, t->total, memory_order_release); + atomic_store_explicit(&t->next_percent_threshold, 101, + memory_order_release); + + ev.type = EV_PROGRESS; + ev.completed = t->total; + ev.total = t->total; + enqueue_event(t, &ev); +} + static bool telemetry_has_pending_events(telemetry_t *t) { if (t->read_index != @@ -519,12 +567,19 @@ static void telemetry_progress(telemetry_t *t, size_t step) step; if (t->percent_step > 0 && t->total > 0) { + if (new_completed >= t->total) { + telemetry_enqueue_final_progress(t); + return; + } + size_t current_pct = (size_t)((new_completed * 100) / t->total); size_t expected = atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); while (current_pct >= expected && expected <= 100) { size_t next = expected + t->percent_step; - if (next > 100) + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) next = 101; // sentinel beyond 100 to stop further emits if (atomic_compare_exchange_strong_explicit( &t->next_percent_threshold, &expected, next, diff --git a/raster/r.buffer/write_map.c b/raster/r.buffer/write_map.c index 7bdcd79f82f..8fed1b3ad9f 100644 --- a/raster/r.buffer/write_map.c +++ b/raster/r.buffer/write_map.c @@ -43,9 +43,10 @@ int write_output_map(char *output, int offset) G_message(_("Writing output raster map <%s>..."), output); ptr = map; - + GPercentContext *ctx = G_percent_context_create(window.rows, 2); for (row = 0; row < window.rows; row++) { - G_percent(row, window.rows, 2); + // G_percent(row + 1, window.rows, 2); + G_percent_r(ctx, row); col = window.cols; if (!offset) { while (col-- > 0) @@ -71,6 +72,7 @@ int write_output_map(char *output, int offset) } G_percent(row, window.rows, 2); + G_percent_context_destroy(ctx); G_free(cell); if (offset) diff --git a/raster/r.texture/execute.c b/raster/r.texture/execute.c index f2234454d73..aa092088b53 100644 --- a/raster/r.texture/execute.c +++ b/raster/r.texture/execute.c @@ -104,13 +104,14 @@ int execute_texture(CELL **data, struct dimensions *dim, else G_message(_("Calculating %s..."), measure_menu[measure_idx[0]].desc); - GPercentContext *ctx = G_percent_context_create_time(nrows, 500); + GPercentContext *ctx = G_percent_context_create(last_row - first_row, 10); #pragma omp parallel private(row, col, i, j, measure, trow) default(shared) { #pragma omp for schedule(static, 1) ordered for (row = first_row; row < last_row; row++) { trow = row % threads; /* Obtain thread row id */ - G_percent_r(ctx, row); + G_percent_r(ctx, row - first_row + 1); + // G_percent(row, nrows, 1); /* initialize the output row */ for (i = 0; i < n_outputs; i++) @@ -165,6 +166,7 @@ int execute_texture(CELL **data, struct dimensions *dim, Rast_put_row(outfd[i], fbuf_threads[0][0], out_data_type); } } + // G_percent(1, 1, 1); G_percent_context_destroy(ctx); for (i = 0; i < threads; i++) { From 574fdc01efdfd01d8c0ef96705ddbf1c695c29ea Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Sun, 29 Mar 2026 15:32:59 +0200 Subject: [PATCH 07/23] new progress.c file and use new G_progress* API --- include/grass/defs/gis.h | 10 +- include/grass/gis.h | 2 +- lib/gis/percent.c | 665 +-------------------------------------- lib/gis/progress.c | 663 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 671 insertions(+), 669 deletions(-) create mode 100644 lib/gis/progress.c diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index 06a712589de..a5ccfd770b2 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -663,10 +663,6 @@ int G_stat(const char *, struct stat *); int G_owner(const char *); /* percent.c */ -GPercentContext *G_percent_context_create(size_t, size_t); -GPercentContext *G_percent_context_create_time(size_t, long); -void G_percent_context_destroy(GPercentContext *); -void G_percent_r(GPercentContext *, size_t); void G_percent(long, long, int); void G_percent_old(long, long, int); void G_percent_reset(void); @@ -674,6 +670,12 @@ void G_progress(long, int); void G_set_percent_routine(int (*)(int)); void G_unset_percent_routine(void); +/* progress.c */ +GProgressContext *G_progress_context_create(size_t, size_t); +GProgressContext *G_progress_context_create_time(size_t, long); +void G_progress_context_destroy(GProgressContext *); +void G_progress_update(GProgressContext *, size_t); + /* popen.c */ void G_popen_clear(struct Popen *); FILE *G_popen_write(struct Popen *, const char *, const char **); diff --git a/include/grass/gis.h b/include/grass/gis.h index bc769f94e11..a63b0c486cb 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -727,7 +727,7 @@ struct ilist { int alloc_values; }; -typedef struct GPercentContext GPercentContext; +typedef struct GProgressContext GProgressContext; /*============================== Prototypes ================================*/ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 84bf5a75fc1..d90c9a6aa65 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -10,673 +10,10 @@ \author GRASS Development Team */ -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#define LOG_CAPACITY 1024 -#define LOG_MSG_SIZE 128 -#define TIME_RATE_LIMIT_MS 100 - -typedef enum { EV_LOG, EV_PROGRESS } event_type_t; - -typedef struct { - atomic_bool ready; - event_type_t type; - size_t completed; - size_t total; - char message[LOG_MSG_SIZE]; -} event_t; - -typedef struct { - event_t buffer[LOG_CAPACITY]; - atomic_size_t write_index; - size_t read_index; - atomic_size_t completed; - size_t total; - int info_format; - atomic_long last_progress_ns; - long interval_ns; - size_t percent_step; - atomic_size_t next_percent_threshold; - atomic_bool stop; -} telemetry_t; - -typedef void (*context_progress_fn)(telemetry_t *, size_t); - -struct GPercentContext { - telemetry_t telemetry; - context_progress_fn report_progress; - atomic_bool initialized; - pthread_t consumer_thread; - atomic_bool consumer_started; -}; - -static telemetry_t g_percent_telemetry; -static atomic_bool g_percent_initialized = false; -static atomic_bool g_percent_consumer_started = false; - -static GPercentContext *context_create(size_t, size_t, long); -static bool telemetry_has_pending_events(telemetry_t *); -static void telemetry_init_time(telemetry_t *, size_t, long); -static void telemetry_init_percent(telemetry_t *, size_t, size_t); -static void enqueue_event(telemetry_t *, event_t *); -static void telemetry_enqueue_final_progress(telemetry_t *); -static void telemetry_log(telemetry_t *, const char *); -static void telemetry_set_info_format(telemetry_t *); -static void telemetry_progress(telemetry_t *, size_t); -static void context_progress_percent(telemetry_t *, size_t); -static void context_progress_time(telemetry_t *, size_t); -static void *telemetry_consumer(void *); -static void start_global_percent(size_t, size_t); -static bool output_is_silenced(void); -static long now_ns(void); - -/// Creates an isolated progress-reporting context for concurrent work. -/// -/// The returned context tracks progress for `total_num_elements` items and -/// emits progress updates whenever completion advances by at least -/// `percent_step` percentage points. `total_num_elements` must match the -/// actual number of work units that will be reported through `G_percent_r()`. -/// In particular, callers should pass a completed-work count, not a raw loop -/// index or a larger container size, otherwise the terminal `100%` update may -/// never be reached. If output is enabled by the current runtime -/// configuration, this function also starts the background consumer thread -/// used to flush queued telemetry events. -/// -/// \param total_num_elements Total number of elements to process. -/// \param step Minimum percentage increment that triggers a -/// progress event. -/// \return A newly allocated `GPercentContext`, or `NULL` if output -/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or -/// verbosity level is below `1`. -GPercentContext *G_percent_context_create(size_t total_num_elements, - size_t step) -{ - return context_create(total_num_elements, step, - (step == 0 ? TIME_RATE_LIMIT_MS : 0)); -} - -GPercentContext *G_percent_context_create_time(size_t total_num_elements, - long interval_ms) -{ - return context_create(total_num_elements, 0, interval_ms); -} - -/// Destroys a `GPercentContext` and releases any resources it owns. -/// -/// This function stops the context's background telemetry consumer, waits for -/// the consumer thread to finish when it was started, marks the context as no -/// longer initialized, and frees the context memory. Passing `NULL` is safe and -/// has no effect. -/// -/// \param ctx The progress-reporting context previously created by -/// `G_percent_context_create()`, or `NULL`. -void G_percent_context_destroy(GPercentContext *ctx) -{ - if (!ctx) { - return; - } - - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { - G_free(ctx); - return; - } - - if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= - ctx->telemetry.total && - atomic_load_explicit(&ctx->telemetry.next_percent_threshold, - memory_order_acquire) <= 100) { - telemetry_enqueue_final_progress(&ctx->telemetry); - } - - atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); - - if (atomic_exchange_explicit(&ctx->consumer_started, false, - memory_order_acq_rel)) { - pthread_join(ctx->consumer_thread, NULL); - } - - atomic_store_explicit(&ctx->initialized, false, memory_order_release); - G_free(ctx); -} - -/// Reports progress for an isolated `GPercentContext` instance. -/// -/// This re-entrant variant of `G_percent` is intended for concurrent or -/// context-specific work. It validates that `ctx` is initialized, clamps -/// `current_element` to the valid `0...total` range, and enqueues a progress -/// event only when the computed percentage reaches the next configured -/// threshold for the context. -/// -/// Callers typically create the context with `G_percent_context_create()`, call -/// this function as work advances, and later release resources with -/// `G_percent_context_destroy()`. -/// -/// \param ctx The progress-reporting context created by -/// `G_percent_context_create()`. -/// \param current_element: The current completed element index or count. -void G_percent_r(GPercentContext *ctx, size_t current_element) -{ - if (!ctx) - return; - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) - return; - - telemetry_t *t = &ctx->telemetry; - if (t->total == 0) - return; - - size_t total = t->total; - size_t completed = (current_element < 0) ? 0 : current_element; - if (completed > total) - completed = total; - - atomic_store_explicit(&t->completed, completed, memory_order_release); - ctx->report_progress(t, completed); -} - -/// Reports global progress when completion crosses the next percentage step. -/// -/// This function initializes the shared global telemetry stream on first use, -/// clamps `current_element` into the valid `0...total_num_elements` range, and -/// enqueues a progress update only when the computed percentage reaches the -/// next configured threshold. When progress reaches the total, a terminal -/// `100%` event is always queued and the background consumer is asked to stop -/// after pending events have been flushed. -/// -/// \param current_element The current completed element index or count. -/// \param total_num_elements The total number of elements to process. Values -/// less than or equal to `0` disable reporting. -/// \param percent_step The minimum percentage increment required before a new -/// progress event is emitted. -void G_percent(long current_element, long total_num_elements, int percent_step) -{ - if (total_num_elements <= 0 || output_is_silenced()) - return; - - start_global_percent((size_t)total_num_elements, (size_t)percent_step); - - // If someone initialized with different totals/steps, we keep the first - // ones for simplicity. - - size_t total = (size_t)total_num_elements; - size_t completed = (current_element < 0) ? 0 : (size_t)current_element; - if (completed > total) - completed = total; - - if (g_percent_telemetry.percent_step == 0) - return; // not configured - - atomic_store_explicit(&g_percent_telemetry.completed, completed, - memory_order_release); - - if (completed == total) { - telemetry_enqueue_final_progress(&g_percent_telemetry); - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - return; - } - - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = atomic_load_explicit( - &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + g_percent_telemetry.percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &g_percent_telemetry.next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(&g_percent_telemetry, &ev); - if (completed == total) { - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - } - return; - } - // CAS failed; expected updated, loop continues - } -} - -static GPercentContext *context_create(size_t total_num_elements, size_t step, - long interval_ms) -{ - if (output_is_silenced()) - return NULL; - - GPercentContext *ctx = G_calloc(1, sizeof(*ctx)); - - atomic_init(&ctx->initialized, true); - - assert(step <= 100); - - if (step == 0) { - assert(interval_ms > 0); - telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); - ctx->report_progress = context_progress_time; - } - else { - telemetry_init_percent(&ctx->telemetry, total_num_elements, step); - ctx->report_progress = context_progress_percent; - } - atomic_init(&ctx->consumer_started, false); - - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &ctx->consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, - &ctx->telemetry); - } - - return ctx; -} - -static void context_progress_percent(telemetry_t *t, size_t completed) -{ - size_t total = t->total; - - if (completed == total) { - telemetry_enqueue_final_progress(t); - return; - } - - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = - atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + t->percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &t->next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(t, &ev); - return; - } - } -} - -static void context_progress_time(telemetry_t *t, size_t completed) -{ - if (completed == t->total) { - telemetry_enqueue_final_progress(t); - return; - } - - long now = now_ns(); - long last = - atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); - - if (now - last < t->interval_ns) { - return; - } - if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, - now, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = t->total; - enqueue_event(t, &ev); -} - -/// Consumes queued telemetry events and emits log or progress output until -/// shutdown is requested and the event buffer has been drained. -/// -/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and -/// formatting settings should be consumed. -/// \return `NULL` after the consumer loop exits and any global consumer state -/// has been reset. -static void *telemetry_consumer(void *arg) -{ - telemetry_t *t = arg; - - while (true) { - if (atomic_load_explicit(&t->stop, memory_order_acquire) && - !telemetry_has_pending_events(t)) { - break; - } - - event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; - - if (!atomic_load_explicit(&ev->ready, memory_order_acquire)) { - sched_yield(); - continue; - } - - // handle event - if (ev->type == EV_LOG) { - printf("[LOG] %s\n", ev->message); - } - else if (ev->type == EV_PROGRESS) { - double pct = (ev->total > 0) - ? (double)ev->completed * 100.0 / (double)ev->total - : 0.0; - - switch (t->info_format) { - case G_INFO_FORMAT_STANDARD: - fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); - if ((int)pct == 100) - fprintf(stderr, "\n"); - break; - case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); - fflush(stderr); - break; - case G_INFO_FORMAT_PLAIN: - fprintf(stderr, "%d%s", (int)pct, - ((int)pct == 100 ? "\n" : "..")); - break; - default: - break; - } - } - - // mark slot free - atomic_store_explicit(&ev->ready, false, memory_order_release); - t->read_index++; - } - - if (t == &g_percent_telemetry) { - atomic_store_explicit(&g_percent_consumer_started, false, - memory_order_release); - atomic_store_explicit(&g_percent_initialized, false, - memory_order_release); - } - - return NULL; -} - -static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) -{ - atomic_init(&t->write_index, 0); - t->read_index = 0; - - for (size_t i = 0; i < LOG_CAPACITY; ++i) { - atomic_init(&t->buffer[i].ready, false); - } - - atomic_init(&t->completed, 0); - t->total = total; - telemetry_set_info_format(t); - - atomic_init(&t->last_progress_ns, 0); - t->interval_ns = interval_ms * 1000000L; - - t->percent_step = 0; // 0 => disabled, use time-based if interval_ns > 0 - atomic_init(&t->next_percent_threshold, 0); - - atomic_init(&t->stop, false); -} - -/// Initializes telemetry state for percentage-based progress reporting. -/// -/// Resets the telemetry ring buffer and counters, disables time-based -/// throttling, and configures the next progress event to be emitted when the -/// completed work reaches the first `percent_step` threshold. -/// -/// \param t The telemetry instance to reset and configure. -/// \param total The total number of work units expected for the tracked -/// operation. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. A value of `0` disables percentage-based -/// thresholds. -static void telemetry_init_percent(telemetry_t *t, size_t total, - size_t percent_step) -{ - atomic_init(&t->write_index, 0); - t->read_index = 0; - for (size_t i = 0; i < LOG_CAPACITY; ++i) { - atomic_init(&t->buffer[i].ready, false); - } - atomic_init(&t->completed, 0); - t->total = total; - telemetry_set_info_format(t); - - // disable time-based gating - atomic_init(&t->last_progress_ns, 0); - t->interval_ns = 0; - - // enable percentage-based gating - t->percent_step = percent_step; - size_t first = percent_step > 0 ? percent_step : 0; - atomic_init(&t->next_percent_threshold, first); - - atomic_init(&t->stop, false); -} - -/// Queues a telemetry event into the ring buffer for later consumption. -/// -/// Waits until the destination slot becomes available, copies the event payload -/// into that slot, and then marks the slot as ready using release semantics so -/// readers can safely observe the published event. -/// -/// \param t The telemetry instance that owns the event buffer. -/// \param src The event payload to enqueue. -static void enqueue_event(telemetry_t *t, event_t *src) -{ - size_t idx = - atomic_fetch_add_explicit(&t->write_index, 1, memory_order_relaxed); - - event_t *dst = &t->buffer[idx % LOG_CAPACITY]; - - // wait until slot is free (bounded spin) - while (atomic_load_explicit(&dst->ready, memory_order_acquire)) { - sched_yield(); - } - - // copy payload - *dst = *src; - - // publish - atomic_store_explicit(&dst->ready, true, memory_order_release); -} - -/// Queues a terminal `100%` progress event for a telemetry stream. -/// -/// This helper records the stream as fully completed, disables further -/// percentage-threshold reporting, and enqueues one last progress event with -/// `completed == total` so the consumer can emit the final `100%` update. -/// -/// \param t The telemetry instance to finalize. -static void telemetry_enqueue_final_progress(telemetry_t *t) -{ - event_t ev = {0}; - - atomic_store_explicit(&t->completed, t->total, memory_order_release); - atomic_store_explicit(&t->next_percent_threshold, 101, - memory_order_release); - - ev.type = EV_PROGRESS; - ev.completed = t->total; - ev.total = t->total; - enqueue_event(t, &ev); -} - -static bool telemetry_has_pending_events(telemetry_t *t) -{ - if (t->read_index != - atomic_load_explicit(&t->write_index, memory_order_acquire)) { - return true; - } - - event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; - return atomic_load_explicit(&ev->ready, memory_order_acquire); -} - -static void telemetry_log(telemetry_t *t, const char *msg) -{ - event_t ev = {0}; - ev.type = EV_LOG; - snprintf(ev.message, LOG_MSG_SIZE, "%s", msg); - - enqueue_event(t, &ev); -} - -/// Captures the current GRASS info output format for subsequent telemetry. -/// -/// Reads the process-wide info formatting mode and stores it on the telemetry -/// instance so later progress and log events can format output consistently. -/// -/// \param t The telemetry state that caches the active info format. -static void telemetry_set_info_format(telemetry_t *t) -{ - t->info_format = G_info_format(); -} - -/// Records completed work and enqueues a progress event when the next -/// reportable threshold is reached. -/// -/// The function atomically increments the telemetry's completed counter by -/// `step`, then decides whether to emit a progress event using one of two -/// modes: percent-based reporting when `percent_step` and `total` are -/// configured, or time-based throttling when they are not. Atomic -/// compare-and-swap operations ensure that only one caller emits an event for a -/// given threshold or interval. -/// -/// \param t The telemetry state to update and publish through. -/// \param step The number of newly completed units of work to add. -static void telemetry_progress(telemetry_t *t, size_t step) -{ - size_t new_completed = - atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + - step; - - if (t->percent_step > 0 && t->total > 0) { - if (new_completed >= t->total) { - telemetry_enqueue_final_progress(t); - return; - } - - size_t current_pct = (size_t)((new_completed * 100) / t->total); - size_t expected = atomic_load_explicit(&t->next_percent_threshold, - memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + t->percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; // sentinel beyond 100 to stop further emits - if (atomic_compare_exchange_strong_explicit( - &t->next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - // we won the right to emit at this threshold - break; - } - // CAS failed, expected now contains the latest value; loop to - // re-check - } - // If we didn't advance, nothing to emit - if (current_pct < expected || expected > 100) { - return; - } - } - else { - long now = now_ns(); - long last = - atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); - if (now - last < t->interval_ns) { - return; - } - if (!atomic_compare_exchange_strong_explicit( - &t->last_progress_ns, &last, now, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - } - - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = new_completed; - ev.total = t->total; - - enqueue_event(t, &ev); -} - -/// Initializes shared percent-based telemetry and starts the detached consumer -/// thread once. -/// -/// This function performs one-time global setup for percent progress reporting. -/// Repeated calls return immediately after the initialization state has been -/// set. If output is disabled or the consumer thread cannot be created, no -/// further progress consumer setup is performed. -/// -/// \param total_num_elements The total number of elements used to compute -/// progress percentages. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. -static void start_global_percent(size_t total_num_elements, size_t percent_step) -{ - bool expected_init = false; - if (!atomic_compare_exchange_strong_explicit( - &g_percent_initialized, &expected_init, true, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - - telemetry_init_percent(&g_percent_telemetry, - ((total_num_elements > 0) ? total_num_elements : 0), - ((percent_step > 0) ? percent_step : 0)); - - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &g_percent_consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_t consumer_thread; - pthread_attr_t attr; - - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - if (pthread_create(&consumer_thread, &attr, telemetry_consumer, - &g_percent_telemetry) != 0) { - atomic_store_explicit(&g_percent_consumer_started, false, - memory_order_release); - atomic_store_explicit(&g_percent_initialized, false, - memory_order_release); - } - pthread_attr_destroy(&attr); - } -} - -static bool output_is_silenced(void) -{ - return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); -} - -/// Returns the current UTC time in nanoseconds. -static long now_ns(void) -{ - struct timespec ts; - timespec_get(&ts, TIME_UTC); - return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; -} - -// ---------------------------------------------------------------------------- - static struct state { int prev; int first; diff --git a/lib/gis/progress.c b/lib/gis/progress.c new file mode 100644 index 00000000000..3ed8d9bdd2d --- /dev/null +++ b/lib/gis/progress.c @@ -0,0 +1,663 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_CAPACITY 1024 +#define LOG_MSG_SIZE 128 +#define TIME_RATE_LIMIT_MS 100 + +typedef enum { EV_LOG, EV_PROGRESS } event_type_t; + +typedef struct { + atomic_bool ready; + event_type_t type; + size_t completed; + size_t total; + char message[LOG_MSG_SIZE]; +} event_t; + +typedef struct { + event_t buffer[LOG_CAPACITY]; + atomic_size_t write_index; + size_t read_index; + atomic_size_t completed; + size_t total; + int info_format; + atomic_long last_progress_ns; + long interval_ns; + size_t percent_step; + atomic_size_t next_percent_threshold; + atomic_bool stop; +} telemetry_t; + +typedef void (*context_progress_fn)(telemetry_t *, size_t); + +struct GProgressContext { + telemetry_t telemetry; + context_progress_fn report_progress; + atomic_bool initialized; + pthread_t consumer_thread; + atomic_bool consumer_started; +}; + +static telemetry_t g_percent_telemetry; +static atomic_bool g_percent_initialized = false; +static atomic_bool g_percent_consumer_started = false; + +static GProgressContext *context_create(size_t, size_t, long); +static bool telemetry_has_pending_events(telemetry_t *); +static void telemetry_init_time(telemetry_t *, size_t, long); +static void telemetry_init_percent(telemetry_t *, size_t, size_t); +static void enqueue_event(telemetry_t *, event_t *); +static void telemetry_enqueue_final_progress(telemetry_t *); +static void telemetry_log(telemetry_t *, const char *); +static void telemetry_set_info_format(telemetry_t *); +static void telemetry_progress(telemetry_t *, size_t); +static void context_progress_percent(telemetry_t *, size_t); +static void context_progress_time(telemetry_t *, size_t); +static void *telemetry_consumer(void *); +static void start_global_percent(size_t, size_t); +static bool output_is_silenced(void); +static long now_ns(void); + +/// Creates an isolated progress-reporting context for concurrent work. +/// +/// The returned context tracks progress for `total_num_elements` items and +/// emits progress updates whenever completion advances by at least +/// `percent_step` percentage points. `total_num_elements` must match the +/// actual number of work units that will be reported through +/// `G_progress_update()`. In particular, callers should pass a completed-work +/// count, not a raw loop index or a larger container size, otherwise the +/// terminal `100%` update may never be reached. If output is enabled by the +/// current runtime configuration, this function also starts the background +/// consumer thread used to flush queued telemetry events. +/// +/// \param total_num_elements Total number of elements to process. +/// \param step Minimum percentage increment that triggers a +/// progress event. +/// \return A newly allocated `GPercentContext`, or `NULL` if output +/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or +/// verbosity level is below `1`. +GProgressContext *G_progress_context_create(size_t total_num_elements, + size_t step) +{ + return context_create(total_num_elements, step, + (step == 0 ? TIME_RATE_LIMIT_MS : 0)); +} + +GProgressContext *G_progress_context_create_time(size_t total_num_elements, + long interval_ms) +{ + return context_create(total_num_elements, 0, interval_ms); +} + +/// Destroys a `GPercentContext` and releases any resources it owns. +/// +/// This function stops the context's background telemetry consumer, waits for +/// the consumer thread to finish when it was started, marks the context as no +/// longer initialized, and frees the context memory. Passing `NULL` is safe and +/// has no effect. +/// +/// \param ctx The progress-reporting context previously created by +/// `G_percent_context_create()`, or `NULL`. +void G_progress_context_destroy(GProgressContext *ctx) +{ + if (!ctx) { + return; + } + + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { + G_free(ctx); + return; + } + + if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= + ctx->telemetry.total && + atomic_load_explicit(&ctx->telemetry.next_percent_threshold, + memory_order_acquire) <= 100) { + telemetry_enqueue_final_progress(&ctx->telemetry); + } + + atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); + + if (atomic_exchange_explicit(&ctx->consumer_started, false, + memory_order_acq_rel)) { + pthread_join(ctx->consumer_thread, NULL); + } + + atomic_store_explicit(&ctx->initialized, false, memory_order_release); + G_free(ctx); +} + +/// Reports progress for an isolated `GPercentContext` instance. +/// +/// This re-entrant variant of `G_percent` is intended for concurrent or +/// context-specific work. It validates that `ctx` is initialized, clamps +/// `current_element` to the valid `0...total` range, and enqueues a progress +/// event only when the computed percentage reaches the next configured +/// threshold for the context. +/// +/// Callers typically create the context with `G_percent_context_create()`, call +/// this function as work advances, and later release resources with +/// `G_percent_context_destroy()`. +/// +/// \param ctx The progress-reporting context created by +/// `G_percent_context_create()`. +/// \param completed: The current completed element index or count. +void G_progress_update(GProgressContext *ctx, size_t completed) +{ + if (!ctx) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + + telemetry_t *t = &ctx->telemetry; + if (t->total == 0) + return; + + size_t total = t->total; + if (completed > total) + completed = total; + + atomic_store_explicit(&t->completed, completed, memory_order_release); + ctx->report_progress(t, completed); +} + +/// Reports global progress when completion crosses the next percentage step. +/// +/// This function initializes the shared global telemetry stream on first use, +/// clamps `current_element` into the valid `0...total_num_elements` range, and +/// enqueues a progress update only when the computed percentage reaches the +/// next configured threshold. When progress reaches the total, a terminal +/// `100%` event is always queued and the background consumer is asked to stop +/// after pending events have been flushed. +/// +/// \param current_element The current completed element index or count. +/// \param total_num_elements The total number of elements to process. Values +/// less than or equal to `0` disable reporting. +/// \param percent_step The minimum percentage increment required before a new +/// progress event is emitted. +void G_percent(long current_element, long total_num_elements, int percent_step) +{ + if (total_num_elements <= 0 || output_is_silenced()) + return; + + start_global_percent((size_t)total_num_elements, (size_t)percent_step); + + // If someone initialized with different totals/steps, we keep the first + // ones for simplicity. + + size_t total = (size_t)total_num_elements; + size_t completed = (current_element < 0) ? 0 : (size_t)current_element; + if (completed > total) + completed = total; + + if (g_percent_telemetry.percent_step == 0) + return; // not configured + + atomic_store_explicit(&g_percent_telemetry.completed, completed, + memory_order_release); + + if (completed == total) { + telemetry_enqueue_final_progress(&g_percent_telemetry); + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + return; + } + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = atomic_load_explicit( + &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + g_percent_telemetry.percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &g_percent_telemetry.next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(&g_percent_telemetry, &ev); + if (completed == total) { + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + } + return; + } + // CAS failed; expected updated, loop continues + } +} + +static GProgressContext *context_create(size_t total_num_elements, size_t step, + long interval_ms) +{ + if (output_is_silenced()) + return NULL; + + GProgressContext *ctx = G_calloc(1, sizeof(*ctx)); + + atomic_init(&ctx->initialized, true); + + assert(step <= 100); + + if (step == 0) { + assert(interval_ms > 0); + telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); + ctx->report_progress = context_progress_time; + } + else { + telemetry_init_percent(&ctx->telemetry, total_num_elements, step); + ctx->report_progress = context_progress_percent; + } + atomic_init(&ctx->consumer_started, false); + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &ctx->consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, + &ctx->telemetry); + } + + return ctx; +} + +static void context_progress_percent(telemetry_t *t, size_t completed) +{ + size_t total = t->total; + + if (completed == total) { + telemetry_enqueue_final_progress(t); + return; + } + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = + atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(t, &ev); + return; + } + } +} + +static void context_progress_time(telemetry_t *t, size_t completed) +{ + if (completed == t->total) { + telemetry_enqueue_final_progress(t); + return; + } + + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, + now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = t->total; + enqueue_event(t, &ev); +} + +/// Consumes queued telemetry events and emits log or progress output until +/// shutdown is requested and the event buffer has been drained. +/// +/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and +/// formatting settings should be consumed. +/// \return `NULL` after the consumer loop exits and any global consumer state +/// has been reset. +static void *telemetry_consumer(void *arg) +{ + telemetry_t *t = arg; + + while (true) { + if (atomic_load_explicit(&t->stop, memory_order_acquire) && + !telemetry_has_pending_events(t)) { + break; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + + if (!atomic_load_explicit(&ev->ready, memory_order_acquire)) { + sched_yield(); + continue; + } + + // handle event + if (ev->type == EV_LOG) { + printf("[LOG] %s\n", ev->message); + } + else if (ev->type == EV_PROGRESS) { + double pct = (ev->total > 0) + ? (double)ev->completed * 100.0 / (double)ev->total + : 0.0; + + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); + if ((int)pct == 100) + fprintf(stderr, "\n"); + break; + case G_INFO_FORMAT_GUI: + fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); + fflush(stderr); + break; + case G_INFO_FORMAT_PLAIN: + fprintf(stderr, "%d%s", (int)pct, + ((int)pct == 100 ? "\n" : "..")); + break; + default: + break; + } + } + + // mark slot free + atomic_store_explicit(&ev->ready, false, memory_order_release); + t->read_index++; + } + + if (t == &g_percent_telemetry) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + } + + return NULL; +} + +static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = interval_ms * 1000000L; + + t->percent_step = 0; // 0 => disabled, use time-based if interval_ns > 0 + atomic_init(&t->next_percent_threshold, 0); + + atomic_init(&t->stop, false); +} + +/// Initializes telemetry state for percentage-based progress reporting. +/// +/// Resets the telemetry ring buffer and counters, disables time-based +/// throttling, and configures the next progress event to be emitted when the +/// completed work reaches the first `percent_step` threshold. +/// +/// \param t The telemetry instance to reset and configure. +/// \param total The total number of work units expected for the tracked +/// operation. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. A value of `0` disables percentage-based +/// thresholds. +static void telemetry_init_percent(telemetry_t *t, size_t total, + size_t percent_step) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + + // disable time-based gating + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = 0; + + // enable percentage-based gating + t->percent_step = percent_step; + size_t first = percent_step > 0 ? percent_step : 0; + atomic_init(&t->next_percent_threshold, first); + + atomic_init(&t->stop, false); +} + +/// Queues a telemetry event into the ring buffer for later consumption. +/// +/// Waits until the destination slot becomes available, copies the event payload +/// into that slot, and then marks the slot as ready using release semantics so +/// readers can safely observe the published event. +/// +/// \param t The telemetry instance that owns the event buffer. +/// \param src The event payload to enqueue. +static void enqueue_event(telemetry_t *t, event_t *src) +{ + size_t idx = + atomic_fetch_add_explicit(&t->write_index, 1, memory_order_relaxed); + + event_t *dst = &t->buffer[idx % LOG_CAPACITY]; + + // wait until slot is free (bounded spin) + while (atomic_load_explicit(&dst->ready, memory_order_acquire)) { + sched_yield(); + } + + // copy payload + *dst = *src; + + // publish + atomic_store_explicit(&dst->ready, true, memory_order_release); +} + +/// Queues a terminal `100%` progress event for a telemetry stream. +/// +/// This helper records the stream as fully completed, disables further +/// percentage-threshold reporting, and enqueues one last progress event with +/// `completed == total` so the consumer can emit the final `100%` update. +/// +/// \param t The telemetry instance to finalize. +static void telemetry_enqueue_final_progress(telemetry_t *t) +{ + event_t ev = {0}; + + atomic_store_explicit(&t->completed, t->total, memory_order_release); + atomic_store_explicit(&t->next_percent_threshold, 101, + memory_order_release); + + ev.type = EV_PROGRESS; + ev.completed = t->total; + ev.total = t->total; + enqueue_event(t, &ev); +} + +static bool telemetry_has_pending_events(telemetry_t *t) +{ + if (t->read_index != + atomic_load_explicit(&t->write_index, memory_order_acquire)) { + return true; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + return atomic_load_explicit(&ev->ready, memory_order_acquire); +} + +static void telemetry_log(telemetry_t *t, const char *msg) +{ + event_t ev = {0}; + ev.type = EV_LOG; + snprintf(ev.message, LOG_MSG_SIZE, "%s", msg); + + enqueue_event(t, &ev); +} + +/// Captures the current GRASS info output format for subsequent telemetry. +/// +/// Reads the process-wide info formatting mode and stores it on the telemetry +/// instance so later progress and log events can format output consistently. +/// +/// \param t The telemetry state that caches the active info format. +static void telemetry_set_info_format(telemetry_t *t) +{ + t->info_format = G_info_format(); +} + +/// Records completed work and enqueues a progress event when the next +/// reportable threshold is reached. +/// +/// The function atomically increments the telemetry's completed counter by +/// `step`, then decides whether to emit a progress event using one of two +/// modes: percent-based reporting when `percent_step` and `total` are +/// configured, or time-based throttling when they are not. Atomic +/// compare-and-swap operations ensure that only one caller emits an event for a +/// given threshold or interval. +/// +/// \param t The telemetry state to update and publish through. +/// \param step The number of newly completed units of work to add. +static void telemetry_progress(telemetry_t *t, size_t step) +{ + size_t new_completed = + atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + + step; + + if (t->percent_step > 0 && t->total > 0) { + if (new_completed >= t->total) { + telemetry_enqueue_final_progress(t); + return; + } + + size_t current_pct = (size_t)((new_completed * 100) / t->total); + size_t expected = atomic_load_explicit(&t->next_percent_threshold, + memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; // sentinel beyond 100 to stop further emits + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + // we won the right to emit at this threshold + break; + } + // CAS failed, expected now contains the latest value; loop to + // re-check + } + // If we didn't advance, nothing to emit + if (current_pct < expected || expected > 100) { + return; + } + } + else { + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit( + &t->last_progress_ns, &last, now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = new_completed; + ev.total = t->total; + + enqueue_event(t, &ev); +} + +/// Initializes shared percent-based telemetry and starts the detached consumer +/// thread once. +/// +/// This function performs one-time global setup for percent progress reporting. +/// Repeated calls return immediately after the initialization state has been +/// set. If output is disabled or the consumer thread cannot be created, no +/// further progress consumer setup is performed. +/// +/// \param total_num_elements The total number of elements used to compute +/// progress percentages. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. +static void start_global_percent(size_t total_num_elements, size_t percent_step) +{ + bool expected_init = false; + if (!atomic_compare_exchange_strong_explicit( + &g_percent_initialized, &expected_init, true, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + telemetry_init_percent(&g_percent_telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + ((percent_step > 0) ? percent_step : 0)); + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &g_percent_consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_t consumer_thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&consumer_thread, &attr, telemetry_consumer, + &g_percent_telemetry) != 0) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + } + pthread_attr_destroy(&attr); + } +} + +static bool output_is_silenced(void) +{ + return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); +} + +/// Returns the current UTC time in nanoseconds. +static long now_ns(void) +{ + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +} From bfabc1bec93a47d3f83817ab0a38e7e233a516b8 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Sun, 29 Mar 2026 17:44:59 +0200 Subject: [PATCH 08/23] introduce sink --- include/grass/defs/gis.h | 4 ++ include/grass/gis.h | 18 +++++ lib/gis/percent.c | 4 +- lib/gis/progress.c | 152 ++++++++++++++++++++++++++++++++++----- 4 files changed, 157 insertions(+), 21 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index a5ccfd770b2..fffa28138b4 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -675,6 +675,10 @@ GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); void G_progress_context_destroy(GProgressContext *); void G_progress_update(GProgressContext *, size_t); +// Sink setters (global and per-context) +void G_percent_set_sink(const GProgressSink *sink); +void G_progress_context_set_sink(GProgressContext *ctx, + const GProgressSink *sink); /* popen.c */ void G_popen_clear(struct Popen *); diff --git a/include/grass/gis.h b/include/grass/gis.h index a63b0c486cb..d9737a1b8a7 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -20,6 +20,7 @@ /*============================= Include Files ==============================*/ /* System include files */ +#include #include #include @@ -728,6 +729,23 @@ struct ilist { }; typedef struct GProgressContext GProgressContext; +// Sink-based callback protocol for decoupled output +typedef struct { + size_t completed; + size_t total; + double percent; // 0.0 ... 100.0 + bool is_terminal; // completed >= total and total > 0 +} GProgressEvent; + +typedef void (*GProgressCallback)(const GProgressEvent *event, void *user_data); + +typedef void (*GLogCallback)(const char *message, void *user_data); + +typedef struct { + GProgressCallback on_progress; // optional + GLogCallback on_log; // optional + void *user_data; // opaque sink context +} GProgressSink; /*============================== Prototypes ================================*/ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index d90c9a6aa65..0300f6e5184 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -189,7 +189,7 @@ void G_progress(long n, int s) \param percent_routine routine will be called like this: percent_routine(x) */ -void G_set_percent_routine(int (*percent_routine)(int)) +void G_set_percent_routine_old(int (*percent_routine)(int)) { ext_percent = percent_routine; } @@ -200,7 +200,7 @@ void G_set_percent_routine(int (*percent_routine)(int)) Percentage progress messages are printed directly to stderr. */ -void G_unset_percent_routine(void) +void G_unset_percent_routine_old(void) { ext_percent = NULL; } diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 3ed8d9bdd2d..8dbda9386d4 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -16,11 +16,11 @@ typedef enum { EV_LOG, EV_PROGRESS } event_type_t; typedef struct { - atomic_bool ready; event_type_t type; size_t completed; size_t total; char message[LOG_MSG_SIZE]; + atomic_bool ready; } event_t; typedef struct { @@ -35,6 +35,8 @@ typedef struct { size_t percent_step; atomic_size_t next_percent_threshold; atomic_bool stop; + GProgressSink + sink; // optional sink; if callbacks are NULL, fall back to info_format } telemetry_t; typedef void (*context_progress_fn)(telemetry_t *, size_t); @@ -42,6 +44,7 @@ typedef void (*context_progress_fn)(telemetry_t *, size_t); struct GProgressContext { telemetry_t telemetry; context_progress_fn report_progress; + GProgressSink sink; // per-context override (optional) atomic_bool initialized; pthread_t consumer_thread; atomic_bool consumer_started; @@ -50,6 +53,7 @@ struct GProgressContext { static telemetry_t g_percent_telemetry; static atomic_bool g_percent_initialized = false; static atomic_bool g_percent_consumer_started = false; +static GProgressSink g_percent_sink = {0}; static GProgressContext *context_create(size_t, size_t, long); static bool telemetry_has_pending_events(telemetry_t *); @@ -67,6 +71,16 @@ static void start_global_percent(size_t, size_t); static bool output_is_silenced(void); static long now_ns(void); +// Legacy compatibility: adapter for void (*fn)(int) +static void legacy_percent_adapter(const GProgressEvent *e, void *ud) +{ + void (*fn)(int) = (void (*)(int))ud; + if (fn) { + int pct = (int)(e->percent); + fn(pct); + } +} + /// Creates an isolated progress-reporting context for concurrent work. /// /// The returned context tracks progress for `total_num_elements` items and @@ -136,6 +150,38 @@ void G_progress_context_destroy(GProgressContext *ctx) G_free(ctx); } +void G_progress_context_set_sink(GProgressContext *ctx, + const GProgressSink *sink) +{ + if (!ctx) + return; + if (sink) { + ctx->sink = *sink; + } + else { + ctx->sink.on_progress = NULL; + ctx->sink.on_log = NULL; + ctx->sink.user_data = NULL; + } + // update telemetry copy; safe because sink is read-only by consumer after + // set + ctx->telemetry.sink = ctx->sink; +} + +void G_percent_set_sink(const GProgressSink *sink) +{ + if (sink) { + g_percent_sink = *sink; + } + else { + g_percent_sink.on_progress = NULL; + g_percent_sink.on_log = NULL; + g_percent_sink.user_data = NULL; + } + // apply immediately to global telemetry if initialized + g_percent_telemetry.sink = g_percent_sink; +} + /// Reports progress for an isolated `GPercentContext` instance. /// /// This re-entrant variant of `G_percent` is intended for concurrent or @@ -170,6 +216,33 @@ void G_progress_update(GProgressContext *ctx, size_t completed) ctx->report_progress(t, completed); } +// Compatibility layer for legacy percent routine API +void G_set_percent_routine(int (*fn)(int)) +{ + // The historical signature in gis.h declares int (*)(int), but actual + // implementers often used void(*)(int). We accept int-returning and ignore + // the return value. + if (!fn) { + // Reset to default behavior + G_percent_set_sink(NULL); + return; + } + // Wrap the legacy function pointer in a sink that casts and calls with + // percent + GProgressSink s = {0}; + s.on_progress = legacy_percent_adapter; + // Store the function pointer in user_data with a cast that preserves + // address + s.user_data = (void *)fn; + G_percent_set_sink(&s); +} + +void G_unset_percent_routine(void) +{ + // Reset to default (env-driven G_info_format output) + G_percent_set_sink(NULL); +} + /// Reports global progress when completion crosses the next percentage step. /// /// This function initializes the shared global telemetry stream on first use, @@ -260,6 +333,14 @@ static GProgressContext *context_create(size_t total_num_elements, size_t step, telemetry_init_percent(&ctx->telemetry, total_num_elements, step); ctx->report_progress = context_progress_percent; } + + ctx->sink.on_progress = NULL; + ctx->sink.on_log = NULL; + ctx->sink.user_data = NULL; + + // propagate context sink to telemetry by default + ctx->telemetry.sink = ctx->sink; + atomic_init(&ctx->consumer_started, false); bool expected_started = false; @@ -357,29 +438,48 @@ static void *telemetry_consumer(void *arg) // handle event if (ev->type == EV_LOG) { - printf("[LOG] %s\n", ev->message); + if (t->sink.on_log) { + t->sink.on_log(ev->message, t->sink.user_data); + } + else { + // default logging + printf("[LOG] %s\n", ev->message); + } } else if (ev->type == EV_PROGRESS) { double pct = (ev->total > 0) ? (double)ev->completed * 100.0 / (double)ev->total : 0.0; - - switch (t->info_format) { - case G_INFO_FORMAT_STANDARD: - fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); - if ((int)pct == 100) - fprintf(stderr, "\n"); - break; - case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); - fflush(stderr); - break; - case G_INFO_FORMAT_PLAIN: - fprintf(stderr, "%d%s", (int)pct, - ((int)pct == 100 ? "\n" : "..")); - break; - default: - break; + bool is_terminal = (ev->total > 0 && ev->completed >= ev->total); + + if (t->sink.on_progress) { + GProgressEvent pe = { + .completed = ev->completed, + .total = ev->total, + .percent = pct, + .is_terminal = is_terminal, + }; + t->sink.on_progress(&pe, t->sink.user_data); + } + else { + // Default rendering honors info_format + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); + if ((int)pct == 100) + fprintf(stderr, "\n"); + break; + case G_INFO_FORMAT_GUI: + fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); + fflush(stderr); + break; + case G_INFO_FORMAT_PLAIN: + fprintf(stderr, "%d%s", (int)pct, + ((int)pct == 100 ? "\n" : "..")); + break; + default: + break; + } } } @@ -393,6 +493,7 @@ static void *telemetry_consumer(void *arg) memory_order_release); atomic_store_explicit(&g_percent_initialized, false, memory_order_release); + // keep g_percent_sink as-is; no change needed on shutdown } return NULL; @@ -418,6 +519,11 @@ static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) atomic_init(&t->next_percent_threshold, 0); atomic_init(&t->stop, false); + + // default: no custom sink; callbacks NULL imply fallback to info_format + t->sink.on_progress = NULL; + t->sink.on_log = NULL; + t->sink.user_data = NULL; } /// Initializes telemetry state for percentage-based progress reporting. @@ -454,6 +560,11 @@ static void telemetry_init_percent(telemetry_t *t, size_t total, atomic_init(&t->next_percent_threshold, first); atomic_init(&t->stop, false); + + // default: no custom sink; callbacks NULL imply fallback to info_format + t->sink.on_progress = NULL; + t->sink.on_log = NULL; + t->sink.user_data = NULL; } /// Queues a telemetry event into the ring buffer for later consumption. @@ -629,6 +740,9 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) ((total_num_elements > 0) ? total_num_elements : 0), ((percent_step > 0) ? percent_step : 0)); + // attach current global sink (may be empty for default behavior) + g_percent_telemetry.sink = g_percent_sink; + bool expected_started = false; if (atomic_compare_exchange_strong_explicit( &g_percent_consumer_started, &expected_started, true, From 68816a886be2fbf468ce128c1622895ede430e55 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Sun, 29 Mar 2026 18:09:48 +0200 Subject: [PATCH 09/23] add old G_progress counter --- include/grass/defs/gis.h | 3 + lib/gis/percent.c | 2 +- lib/gis/progress.c | 117 +++++++++++++++++++++++++++++++++------ 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index fffa28138b4..db02f4df9ce 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -679,6 +679,9 @@ void G_progress_update(GProgressContext *, size_t); void G_percent_set_sink(const GProgressSink *sink); void G_progress_context_set_sink(GProgressContext *ctx, const GProgressSink *sink); +// Counter-style progress API (unknown total) +void G_progress_increment(GProgressContext *ctx, size_t step); +void G_progress_tick(GProgressContext *ctx); /* popen.c */ void G_popen_clear(struct Popen *); diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 0300f6e5184..ac59351784f 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -155,7 +155,7 @@ void G_percent_reset(void) \return always returns 0 */ -void G_progress(long n, int s) +void G_progress_old(long n, int s) { int format; diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 8dbda9386d4..9b09187f239 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -216,6 +216,21 @@ void G_progress_update(GProgressContext *ctx, size_t completed) ctx->report_progress(t, completed); } +void G_progress_increment(GProgressContext *ctx, size_t step) +{ + if (!ctx || step == 0) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + + telemetry_progress(&ctx->telemetry, step); +} + +void G_progress_tick(GProgressContext *ctx) +{ + G_progress_increment(ctx, 1); +} + // Compatibility layer for legacy percent routine API void G_set_percent_routine(int (*fn)(int)) { @@ -312,6 +327,54 @@ void G_percent(long current_element, long total_num_elements, int percent_step) } } +void G_progress(long n, int s) +{ + // Mirror legacy behavior: emit on multiples of s, and handle first tick + // formatting. We route through the global telemetry so it benefits from the + // consumer thread. + + if (s <= 0 || output_is_silenced()) + return; + + // Initialize global telemetry if needed with percent_step=0 to disable + // percent thresholds + start_global_percent(0, 0); + + // Use time-based gating if an interval is configured; otherwise, we emit + // only on multiples of s. Here, we implement the modulo gating explicitly. + + if (n == s && n == 1) { + // For default sink, legacy prints a leading CR/Newline depending on + // format. We simulate this by enqueueing a LOG event when using default + // sink; custom sinks can ignore. + if (g_percent_telemetry.sink.on_progress == NULL) { + switch (g_percent_telemetry.info_format) { + case G_INFO_FORMAT_PLAIN: + telemetry_log(&g_percent_telemetry, "\n"); + break; + case G_INFO_FORMAT_GUI: + // No-op; GUI variant prints on progress events + break; + default: + // STANDARD and others: carriage return + telemetry_log(&g_percent_telemetry, "\r"); + break; + } + } + return; + } + + if (n % s != 0) + return; + + // For counter mode, we do not know total; publish completed=n, total=0 + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = (n < 0 ? 0 : (size_t)n); + ev.total = 0; // unknown total; consumer/sink can render raw counts + enqueue_event(&g_percent_telemetry, &ev); +} + static GProgressContext *context_create(size_t total_num_elements, size_t step, long interval_ms) { @@ -462,23 +525,43 @@ static void *telemetry_consumer(void *arg) t->sink.on_progress(&pe, t->sink.user_data); } else { - // Default rendering honors info_format - switch (t->info_format) { - case G_INFO_FORMAT_STANDARD: - fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); - if ((int)pct == 100) - fprintf(stderr, "\n"); - break; - case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); - fflush(stderr); - break; - case G_INFO_FORMAT_PLAIN: - fprintf(stderr, "%d%s", (int)pct, - ((int)pct == 100 ? "\n" : "..")); - break; - default: - break; + // Default rendering honors info_format; when total==0, print + // raw counts like legacy G_progress + if (ev->total == 0) { + switch (t->info_format) { + case G_INFO_FORMAT_PLAIN: + fprintf(stderr, "%zu..", ev->completed); + break; + case G_INFO_FORMAT_GUI: + fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", + ev->completed); + fflush(stderr); + break; + case G_INFO_FORMAT_STANDARD: + default: + fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", + ev->completed); + break; + } + } + else { + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); + if ((int)pct == 100) + fprintf(stderr, "\n"); + break; + case G_INFO_FORMAT_GUI: + fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); + fflush(stderr); + break; + case G_INFO_FORMAT_PLAIN: + fprintf(stderr, "%d%s", (int)pct, + ((int)pct == 100 ? "\n" : "..")); + break; + default: + break; + } } } } From bffc9d4b19bd09a6ef6fc3b353575d1e73dd3ec9 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Sun, 29 Mar 2026 18:55:03 +0200 Subject: [PATCH 10/23] add no-op G_percent_reset --- lib/gis/percent.c | 2 +- lib/gis/progress.c | 32 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index ac59351784f..6b6ba1ebf06 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -114,7 +114,7 @@ void G_percent_old(long n, long d, int s) /*! \brief Reset G_percent() to 0%; do not add newline. */ -void G_percent_reset(void) +void G_percent_reset_old(void) { st->prev = -1; st->first = 1; diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 9b09187f239..d9707ec9c12 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -70,16 +70,7 @@ static void *telemetry_consumer(void *); static void start_global_percent(size_t, size_t); static bool output_is_silenced(void); static long now_ns(void); - -// Legacy compatibility: adapter for void (*fn)(int) -static void legacy_percent_adapter(const GProgressEvent *e, void *ud) -{ - void (*fn)(int) = (void (*)(int))ud; - if (fn) { - int pct = (int)(e->percent); - fn(pct); - } -} +static void legacy_percent_adapter(const GProgressEvent *e, void *ud); /// Creates an isolated progress-reporting context for concurrent work. /// @@ -163,8 +154,8 @@ void G_progress_context_set_sink(GProgressContext *ctx, ctx->sink.on_log = NULL; ctx->sink.user_data = NULL; } - // update telemetry copy; safe because sink is read-only by consumer after - // set + // update telemetry copy; safe because sink is read-only + // by consumer after set ctx->telemetry.sink = ctx->sink; } @@ -231,6 +222,13 @@ void G_progress_tick(GProgressContext *ctx) G_progress_increment(ctx, 1); } +// Transitional no-op: retained for compatibility with legacy callers +void G_percent_reset(void) +{ + // No global state to reset in the concurrent API. + // Kept to avoid breaking legacy code paths that call G_percent_reset(). +} + // Compatibility layer for legacy percent routine API void G_set_percent_routine(int (*fn)(int)) { @@ -858,3 +856,13 @@ static long now_ns(void) timespec_get(&ts, TIME_UTC); return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; } + +// Legacy compatibility: adapter for void (*fn)(int) +static void legacy_percent_adapter(const GProgressEvent *e, void *ud) +{ + void (*fn)(int) = (void (*)(int))ud; + if (fn) { + int pct = (int)(e->percent); + fn(pct); + } +} From ed777465f8ee890db9b5505e85edc25debd92e48 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Mon, 30 Mar 2026 09:17:58 +0200 Subject: [PATCH 11/23] use sinks to the default modes --- lib/gis/progress.c | 150 ++++++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 44 deletions(-) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index d9707ec9c12..b131fcf8311 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -63,6 +63,7 @@ static void enqueue_event(telemetry_t *, event_t *); static void telemetry_enqueue_final_progress(telemetry_t *); static void telemetry_log(telemetry_t *, const char *); static void telemetry_set_info_format(telemetry_t *); +static void telemetry_install_default_sink(telemetry_t *t); static void telemetry_progress(telemetry_t *, size_t); static void context_progress_percent(telemetry_t *, size_t); static void context_progress_time(telemetry_t *, size_t); @@ -71,6 +72,10 @@ static void start_global_percent(size_t, size_t); static bool output_is_silenced(void); static long now_ns(void); static void legacy_percent_adapter(const GProgressEvent *e, void *ud); +static void sink_progress_standard(const GProgressEvent *e, void *ud); +static void sink_progress_plain(const GProgressEvent *e, void *ud); +static void sink_progress_gui(const GProgressEvent *e, void *ud); +static void sink_log_default(const char *message, void *ud); /// Creates an isolated progress-reporting context for concurrent work. /// @@ -169,8 +174,20 @@ void G_percent_set_sink(const GProgressSink *sink) g_percent_sink.on_log = NULL; g_percent_sink.user_data = NULL; } - // apply immediately to global telemetry if initialized - g_percent_telemetry.sink = g_percent_sink; + + // apply to global telemetry if initialized + if (atomic_load_explicit(&g_percent_initialized, memory_order_acquire)) { + if (g_percent_sink.on_progress || g_percent_sink.on_log) { + g_percent_telemetry.sink = g_percent_sink; + } + else { + // reinstall defaults based on current info_format + g_percent_telemetry.sink.on_progress = NULL; + g_percent_telemetry.sink.on_log = NULL; + g_percent_telemetry.sink.user_data = NULL; + telemetry_install_default_sink(&g_percent_telemetry); + } + } } /// Reports progress for an isolated `GPercentContext` instance. @@ -244,8 +261,7 @@ void G_set_percent_routine(int (*fn)(int)) // percent GProgressSink s = {0}; s.on_progress = legacy_percent_adapter; - // Store the function pointer in user_data with a cast that preserves - // address + // Store the function pointer in user_data s.user_data = (void *)fn; G_percent_set_sink(&s); } @@ -503,8 +519,7 @@ static void *telemetry_consumer(void *arg) t->sink.on_log(ev->message, t->sink.user_data); } else { - // default logging - printf("[LOG] %s\n", ev->message); + sink_log_default(ev->message, NULL); } } else if (ev->type == EV_PROGRESS) { @@ -523,43 +538,17 @@ static void *telemetry_consumer(void *arg) t->sink.on_progress(&pe, t->sink.user_data); } else { - // Default rendering honors info_format; when total==0, print - // raw counts like legacy G_progress - if (ev->total == 0) { - switch (t->info_format) { - case G_INFO_FORMAT_PLAIN: - fprintf(stderr, "%zu..", ev->completed); - break; - case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", - ev->completed); - fflush(stderr); - break; - case G_INFO_FORMAT_STANDARD: - default: - fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", - ev->completed); - break; - } - } - else { - switch (t->info_format) { - case G_INFO_FORMAT_STANDARD: - fprintf(stderr, "%4d%%\b\b\b\b\b", (int)pct); - if ((int)pct == 100) - fprintf(stderr, "\n"); - break; - case G_INFO_FORMAT_GUI: - fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", (int)pct); - fflush(stderr); - break; - case G_INFO_FORMAT_PLAIN: - fprintf(stderr, "%d%s", (int)pct, - ((int)pct == 100 ? "\n" : "..")); - break; - default: - break; - } + // Ensure defaults exist (defensive, should already be set at + // init) + telemetry_install_default_sink(t); + if (t->sink.on_progress) { + GProgressEvent pe = { + .completed = ev->completed, + .total = ev->total, + .percent = pct, + .is_terminal = is_terminal, + }; + t->sink.on_progress(&pe, t->sink.user_data); } } } @@ -605,6 +594,7 @@ static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) t->sink.on_progress = NULL; t->sink.on_log = NULL; t->sink.user_data = NULL; + telemetry_install_default_sink(t); } /// Initializes telemetry state for percentage-based progress reporting. @@ -646,6 +636,7 @@ static void telemetry_init_percent(telemetry_t *t, size_t total, t->sink.on_progress = NULL; t->sink.on_log = NULL; t->sink.user_data = NULL; + telemetry_install_default_sink(t); } /// Queues a telemetry event into the ring buffer for later consumption. @@ -796,6 +787,30 @@ static void telemetry_progress(telemetry_t *t, size_t step) enqueue_event(t, &ev); } +static void telemetry_install_default_sink(telemetry_t *t) +{ + // Only set defaults if no custom sink is present + if (t->sink.on_progress || t->sink.on_log) + return; + + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + t->sink.on_progress = sink_progress_standard; + break; + case G_INFO_FORMAT_GUI: + t->sink.on_progress = sink_progress_gui; + break; + case G_INFO_FORMAT_PLAIN: + t->sink.on_progress = sink_progress_plain; + break; + default: + t->sink.on_progress = NULL; // silent/no output + break; + } + t->sink.on_log = sink_log_default; + t->sink.user_data = NULL; +} + /// Initializes shared percent-based telemetry and starts the detached consumer /// thread once. /// @@ -822,7 +837,9 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) ((percent_step > 0) ? percent_step : 0)); // attach current global sink (may be empty for default behavior) - g_percent_telemetry.sink = g_percent_sink; + if (g_percent_sink.on_progress || g_percent_sink.on_log) { + g_percent_telemetry.sink = g_percent_sink; + } // else keep defaults installed by telemetry_init_percent bool expected_started = false; if (atomic_compare_exchange_strong_explicit( @@ -866,3 +883,48 @@ static void legacy_percent_adapter(const GProgressEvent *e, void *ud) fn(pct); } } + +// Internal default sinks for different G_info_format modes +static void sink_progress_standard(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", e->completed); + return; + } + int pct = (int)(e->percent); + fprintf(stderr, "%4d%%\b\b\b\b\b", pct); + if (pct == 100) + fprintf(stderr, "\n"); +} + +static void sink_progress_plain(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "%zu..", e->completed); + return; + } + int pct = (int)(e->percent); + fprintf(stderr, "%d%s", pct, (pct == 100 ? "\n" : "..")); +} + +static void sink_progress_gui(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", e->completed); + fflush(stderr); + return; + } + int pct = (int)(e->percent); + fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", pct); + fflush(stderr); +} + +static void sink_log_default(const char *message, void *ud) +{ + (void)ud; + // default logging to stdout + printf("[LOG] %s\n", message); +} From 2e0bb4f92a3b60656eb31eee8f45f8a9138b6e8f Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Mon, 30 Mar 2026 13:52:57 +0200 Subject: [PATCH 12/23] add G_progress_log --- include/grass/defs/gis.h | 1 + lib/gis/progress.c | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index db02f4df9ce..f2feea7cd16 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -675,6 +675,7 @@ GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); void G_progress_context_destroy(GProgressContext *); void G_progress_update(GProgressContext *, size_t); +void G_progress_log(GProgressContext *ctx, const char *message); // Sink setters (global and per-context) void G_percent_set_sink(const GProgressSink *sink); void G_progress_context_set_sink(GProgressContext *ctx, diff --git a/lib/gis/progress.c b/lib/gis/progress.c index b131fcf8311..8f6c73073c7 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -239,6 +239,15 @@ void G_progress_tick(GProgressContext *ctx) G_progress_increment(ctx, 1); } +void G_progress_log(GProgressContext *ctx, const char *message) +{ + if (!ctx || !message) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + telemetry_log(&ctx->telemetry, message); +} + // Transitional no-op: retained for compatibility with legacy callers void G_percent_reset(void) { From 20686d0b68c53fe7e65e7297e18e81e8929ba373 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Tue, 31 Mar 2026 21:50:56 +0200 Subject: [PATCH 13/23] various fixes and docs --- include/grass/defs/gis.h | 6 +- include/grass/gis.h | 41 +++++++- lib/gis/percent.c | 17 +-- lib/gis/progress.c | 201 ++++++++++++++++++++++++++++++------ raster/r.buffer/write_map.c | 26 ++++- raster/r.texture/execute.c | 6 +- 6 files changed, 243 insertions(+), 54 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index f2feea7cd16..37e5fc6f8cf 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -664,25 +664,23 @@ int G_owner(const char *); /* percent.c */ void G_percent(long, long, int); -void G_percent_old(long, long, int); void G_percent_reset(void); void G_progress(long, int); void G_set_percent_routine(int (*)(int)); void G_unset_percent_routine(void); /* progress.c */ +#if defined(G_USE_PROGRESS_NG) GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); void G_progress_context_destroy(GProgressContext *); void G_progress_update(GProgressContext *, size_t); void G_progress_log(GProgressContext *ctx, const char *message); -// Sink setters (global and per-context) -void G_percent_set_sink(const GProgressSink *sink); void G_progress_context_set_sink(GProgressContext *ctx, const GProgressSink *sink); -// Counter-style progress API (unknown total) void G_progress_increment(GProgressContext *ctx, size_t step); void G_progress_tick(GProgressContext *ctx); +#endif // G_USE_PROGRESS_NG /* popen.c */ void G_popen_clear(struct Popen *); diff --git a/include/grass/gis.h b/include/grass/gis.h index d9737a1b8a7..eef4eae5a51 100644 --- a/include/grass/gis.h +++ b/include/grass/gis.h @@ -60,6 +60,26 @@ static const char *GRASS_copyright UNUSED = "GRASS GNU GPL licensed Software"; #define FALLTHROUGH ((void)0) #endif +/*! + \def G_HAS_ATOMICS + \brief A macro that signals whether C11 atomic operations are supported. + */ +#if __STDC_VERSION__ < 201112L || defined(__STDC_NO_ATOMICS__) +#define G_HAS_ATOMICS 0 +#else +#define G_HAS_ATOMICS 1 +#endif + +/*! + \def G_USE_PROGRESS_NG + \brief A macro indicating if concurrency progress reporting can be used. + */ +#if defined(G_HAS_ATOMICS) && defined(HAVE_PTHREAD_H) +#define G_USE_PROGRESS_NG 1 +#else +#define G_USE_PROGRESS_NG 0 +#endif + /* GRASS version, GRASS date, git short hash of last change in GRASS headers * (and anything else in include) */ @@ -728,8 +748,20 @@ struct ilist { int alloc_values; }; +#if defined(G_USE_PROGRESS_NG) +/// Opaque handle that owns telemetry state for one progress-reporting stream. +/// +/// A `GProgressContext` isolates queued events, progress thresholds, and the +/// optional consumer thread used by the `G_progress_context_*()` APIs so that +/// concurrent operations can report progress independently. typedef struct GProgressContext GProgressContext; -// Sink-based callback protocol for decoupled output + +/// Snapshot of a progress update delivered to sink callbacks. +/// +/// `GProgressEvent` reports the completed and total work units for a queued +/// progress emission, together with the derived percentage in the range +/// `0.0 ... 100.0` and a terminal flag that becomes `true` once completion +/// reaches the declared total. typedef struct { size_t completed; size_t total; @@ -738,14 +770,19 @@ typedef struct { } GProgressEvent; typedef void (*GProgressCallback)(const GProgressEvent *event, void *user_data); - typedef void (*GLogCallback)(const char *message, void *user_data); +/// Callback bundle used to receive progress and log output. +/// +/// `GProgressSink` lets callers redirect telemetry emitted by the progress +/// APIs. Each callback is optional; when present, it receives the supplied +/// `user_data` pointer unchanged for every event dispatched through the sink. typedef struct { GProgressCallback on_progress; // optional GLogCallback on_log; // optional void *user_data; // opaque sink context } GProgressSink; +#endif // G_USE_PROGRESS_NG /*============================== Prototypes ================================*/ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 6b6ba1ebf06..f66dca7bfc3 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -11,9 +11,12 @@ \author GRASS Development Team */ -#include #include +#if !defined(G_USE_PROGRESS_NG) + +#include + static struct state { int prev; int first; @@ -58,7 +61,7 @@ static int (*ext_percent)(int); \param d total number of elements \param s increment size */ -void G_percent_old(long n, long d, int s) +void G_percent(long n, long d, int s) { int x, format; @@ -114,7 +117,7 @@ void G_percent_old(long n, long d, int s) /*! \brief Reset G_percent() to 0%; do not add newline. */ -void G_percent_reset_old(void) +void G_percent_reset(void) { st->prev = -1; st->first = 1; @@ -155,7 +158,7 @@ void G_percent_reset_old(void) \return always returns 0 */ -void G_progress_old(long n, int s) +void G_progress(long n, int s) { int format; @@ -189,7 +192,7 @@ void G_progress_old(long n, int s) \param percent_routine routine will be called like this: percent_routine(x) */ -void G_set_percent_routine_old(int (*percent_routine)(int)) +void G_set_percent_routine(int (*percent_routine)(int)) { ext_percent = percent_routine; } @@ -200,7 +203,9 @@ void G_set_percent_routine_old(int (*percent_routine)(int)) Percentage progress messages are printed directly to stderr. */ -void G_unset_percent_routine_old(void) +void G_unset_percent_routine(void) { ext_percent = NULL; } + +#endif // !defined(G_USE_PROGRESS_NG) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 8f6c73073c7..164054f2152 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -1,3 +1,42 @@ +/// \file lib/gis/progress.c +/// +/// Progress reporting and telemetry support for GRASS GIS operations. +/// +/// This file implements the `G_progress_*` API used to report incremental work +/// completion for long-running operations. It supports both percentage-based +/// and time-based progress contexts, plus compatibility wrappers for the legacy +/// global G_percent() and G_progress() entry points. +/// +/// The implementation is organized as a telemetry pipeline. Producer-side API +/// calls update atomic progress state and enqueue ::event_type_t `EV_PROGRESS` +/// or `EV_LOG` records into a bounded ring buffer. A single consumer thread +/// drains that buffer, converts raw records into `GProgressEvent` values, and +/// forwards them either to installed sink callbacks or to default renderers +/// selected from the current G_info_format() mode. +/// +/// Concurrency is designed as multi-producer, single-consumer per telemetry +/// stream. Producers reserve slots with an atomic `write_index`, publish events +/// by setting a per-slot `ready` flag with release semantics, and use atomic +/// compare-and-swap to ensure that only one producer emits a given percent +/// threshold or time-gated update. The consumer advances a non-atomic +/// `read_index`, waits for published slots, processes events in FIFO order, and +/// then marks slots free again. +/// +/// Two lifecycle models are used. Isolated `GProgressContext` instances create +/// a dedicated consumer thread that is joined during destruction. The legacy +/// process-wide G_percent() path initializes one shared telemetry instance and +/// a detached consumer thread on first use. +/// +/// (C) 2026 by the GRASS Development Team +/// +/// SPDX-License-Identifier: GPL-2.0-or-later +/// +/// \author Nicklas Larsson + +#include + +#if defined(G_USE_PROGRESS_NG) + #include #include #include @@ -7,8 +46,6 @@ #include #include -#include - #define LOG_CAPACITY 1024 #define LOG_MSG_SIZE 128 #define TIME_RATE_LIMIT_MS 100 @@ -23,6 +60,11 @@ typedef struct { atomic_bool ready; } event_t; +/// Internal telemetry state shared by the progress producer and consumer. +/// +/// `telemetry_t` owns the ring buffer of queued log/progress events, the +/// counters and thresholds used for time- or percent-based emission, and the +/// sink configuration that determines how flushed events are rendered. typedef struct { event_t buffer[LOG_CAPACITY]; atomic_size_t write_index; @@ -35,8 +77,7 @@ typedef struct { size_t percent_step; atomic_size_t next_percent_threshold; atomic_bool stop; - GProgressSink - sink; // optional sink; if callbacks are NULL, fall back to info_format + GProgressSink sink; // optional sink } telemetry_t; typedef void (*context_progress_fn)(telemetry_t *, size_t); @@ -69,6 +110,7 @@ static void context_progress_percent(telemetry_t *, size_t); static void context_progress_time(telemetry_t *, size_t); static void *telemetry_consumer(void *); static void start_global_percent(size_t, size_t); +static void set_global_sink(const GProgressSink *sink); static bool output_is_silenced(void); static long now_ns(void); static void legacy_percent_adapter(const GProgressEvent *e, void *ud); @@ -102,6 +144,23 @@ GProgressContext *G_progress_context_create(size_t total_num_elements, (step == 0 ? TIME_RATE_LIMIT_MS : 0)); } +/// Creates an isolated progress-reporting context with time-based updates. +/// +/// Unlike `G_progress_context_create()`, which emits progress events when +/// completion crosses percentage thresholds, this variant rate-limits progress +/// emission by elapsed time. The returned context tracks +/// `total_num_elements` work units and reports updates no more frequently than +/// once every `interval_ms` milliseconds while work is in progress. +/// +/// Callers should report monotonically increasing completed-work counts through +/// `G_progress_update()` and destroy the context with +/// `G_progress_context_destroy()` when processing finishes. +/// +/// \param total_num_elements Total number of elements to process. +/// \param interval_ms Minimum time interval, in milliseconds, between emitted +/// progress updates. +/// \return A newly allocated `GProgressContext`, or `NULL` if output is +/// silenced by the current runtime configuration. GProgressContext *G_progress_context_create_time(size_t total_num_elements, long interval_ms) { @@ -146,6 +205,28 @@ void G_progress_context_destroy(GProgressContext *ctx) G_free(ctx); } +/// Sets or clears the output sink used by a progress context. +/// +/// Installs a per-context `GProgressSink` override for progress and log events +/// emitted by `ctx`. When `sink` is non-`NULL`, its callbacks and `user_data` +/// are copied into the context and used by the telemetry consumer. Passing +/// `NULL` clears any custom sink so the context falls back to its default +/// output behavior. +/// +/// \param ctx The progress context to update. If `NULL`, the function has +/// no effect. +/// \param sink The sink configuration to copy into the context, or `NULL` +/// to remove the custom sink. +/// +/// Example: +/// ```c +/// GProgressSink sink = { +/// .on_progress = my_progress_handler, +/// .on_log = my_log_handler, +/// .user_data = my_context, +/// }; +/// G_progress_context_set_sink(progress_ctx, &sink); +/// ``` void G_progress_context_set_sink(GProgressContext *ctx, const GProgressSink *sink) { @@ -164,32 +245,6 @@ void G_progress_context_set_sink(GProgressContext *ctx, ctx->telemetry.sink = ctx->sink; } -void G_percent_set_sink(const GProgressSink *sink) -{ - if (sink) { - g_percent_sink = *sink; - } - else { - g_percent_sink.on_progress = NULL; - g_percent_sink.on_log = NULL; - g_percent_sink.user_data = NULL; - } - - // apply to global telemetry if initialized - if (atomic_load_explicit(&g_percent_initialized, memory_order_acquire)) { - if (g_percent_sink.on_progress || g_percent_sink.on_log) { - g_percent_telemetry.sink = g_percent_sink; - } - else { - // reinstall defaults based on current info_format - g_percent_telemetry.sink.on_progress = NULL; - g_percent_telemetry.sink.on_log = NULL; - g_percent_telemetry.sink.user_data = NULL; - telemetry_install_default_sink(&g_percent_telemetry); - } - } -} - /// Reports progress for an isolated `GPercentContext` instance. /// /// This re-entrant variant of `G_percent` is intended for concurrent or @@ -202,6 +257,23 @@ void G_percent_set_sink(const GProgressSink *sink) /// this function as work advances, and later release resources with /// `G_percent_context_destroy()`. /// +/// Example: +/// ```c +/// size_t n_rows = window.rows; // total number of rows +/// size_t step = 10; // output step, every 10% +/// GProgressContext *ctx = G_progress_context_create(n_rows, step); +/// for (row = 0; row < window.rows; row++) { +/// // costly calculation ... +/// +/// // note: not counting from zero, as for loop never reaches n_rows +/// // and we want to reach 100% +/// size_t completed_row = row + 1; +/// +/// G_progress_update(ctx, completed_row); +/// } +/// G_progress_context_destroy(ctx); +/// ``` +/// /// \param ctx The progress-reporting context created by /// `G_percent_context_create()`. /// \param completed: The current completed element index or count. @@ -263,7 +335,7 @@ void G_set_percent_routine(int (*fn)(int)) // the return value. if (!fn) { // Reset to default behavior - G_percent_set_sink(NULL); + set_global_sink(NULL); return; } // Wrap the legacy function pointer in a sink that casts and calls with @@ -272,13 +344,13 @@ void G_set_percent_routine(int (*fn)(int)) s.on_progress = legacy_percent_adapter; // Store the function pointer in user_data s.user_data = (void *)fn; - G_percent_set_sink(&s); + set_global_sink(&s); } void G_unset_percent_routine(void) { // Reset to default (env-driven G_info_format output) - G_percent_set_sink(NULL); + set_global_sink(NULL); } /// Reports global progress when completion crosses the next percentage step. @@ -398,6 +470,21 @@ void G_progress(long n, int s) enqueue_event(&g_percent_telemetry, &ev); } +/// Creates and initializes a progress reporting context. +/// +/// The created context configures its reporting mode based on `step`. When +/// `step` is `0`, progress updates are emitted using a time-based interval +/// controlled by `interval_ms`. Otherwise, progress updates are emitted at +/// percentage increments defined by `step`. +/// +/// \param total_num_elements Total number of elements expected for the +/// operation being tracked. +/// \param step Percentage increment for reporting progress. A value of `0` +/// selects time-based reporting instead. +/// \param interval_ms Time interval in milliseconds between progress updates +/// when `step` is `0`. +/// \return A newly allocated and initialized `GProgressContext`, or `NULL` +/// if output is currently silenced. static GProgressContext *context_create(size_t total_num_elements, size_t step, long interval_ms) { @@ -870,6 +957,46 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) } } +/// Sets or clears the global sink used by `G_percent` progress reporting. +/// +/// Copies `sink` into the shared global progress configuration used by the +/// legacy `G_percent` API. When `sink` is non-`NULL`, its callbacks and +/// `user_data` are used for subsequent progress and log events. Passing `NULL` +/// clears the custom sink and restores the default output behavior derived from +/// the current runtime info format. +/// +/// If global progress telemetry has already been initialized, the active +/// telemetry sink is updated immediately so later events follow the new +/// configuration. +/// +/// \param sink The sink configuration to install globally, or `NULL` to remove +/// the custom sink and fall back to the default renderer. +static void set_global_sink(const GProgressSink *sink) +{ + if (sink) { + g_percent_sink = *sink; + } + else { + g_percent_sink.on_progress = NULL; + g_percent_sink.on_log = NULL; + g_percent_sink.user_data = NULL; + } + + // apply to global telemetry if initialized + if (atomic_load_explicit(&g_percent_initialized, memory_order_acquire)) { + if (g_percent_sink.on_progress || g_percent_sink.on_log) { + g_percent_telemetry.sink = g_percent_sink; + } + else { + // reinstall defaults based on current info_format + g_percent_telemetry.sink.on_progress = NULL; + g_percent_telemetry.sink.on_log = NULL; + g_percent_telemetry.sink.user_data = NULL; + telemetry_install_default_sink(&g_percent_telemetry); + } + } +} + static bool output_is_silenced(void) { return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); @@ -927,7 +1054,11 @@ static void sink_progress_gui(const GProgressEvent *e, void *ud) return; } int pct = (int)(e->percent); - fprintf(stderr, "GRASS_INFO_PERCENT: %d\n", pct); + + int comp = (int)e->completed; + int tot = (int)e->total; + + fprintf(stderr, "GRASS_INFO_PERCENT: %d (%d/%d)\n", pct, comp, tot); fflush(stderr); } @@ -937,3 +1068,5 @@ static void sink_log_default(const char *message, void *ud) // default logging to stdout printf("[LOG] %s\n", message); } + +#endif // defined(G_USE_PROGRESS_NG) diff --git a/raster/r.buffer/write_map.c b/raster/r.buffer/write_map.c index 8fed1b3ad9f..c35c607458b 100644 --- a/raster/r.buffer/write_map.c +++ b/raster/r.buffer/write_map.c @@ -22,9 +22,19 @@ #include "distance.h" #include #include +#include /* write out result */ +static void sink_progress_buffer(const GProgressEvent *e, void *ud) +{ + (void)ud; + double pct = (int)(e->percent); + int comp = (int)e->completed; + int tot = (int)e->total; + printf("[PROGRESS] %4f (%d/%d)\n", pct, comp, tot); +} + int write_output_map(char *output, int offset) { int fd_in = 0, fd_out; @@ -34,6 +44,9 @@ int write_output_map(char *output, int offset) register MAPTYPE *ptr; int k; + GProgressSink sink = { + .on_progress = sink_progress_buffer, .on_log = NULL, .user_data = NULL}; + fd_out = Rast_open_c_new(output); if (offset) @@ -43,10 +56,13 @@ int write_output_map(char *output, int offset) G_message(_("Writing output raster map <%s>..."), output); ptr = map; - GPercentContext *ctx = G_percent_context_create(window.rows, 2); + GProgressContext *ctx = G_progress_context_create_time(window.rows, 100); + G_progress_context_set_sink(ctx, &sink); + G_progress_log(ctx, "starting:"); + for (row = 0; row < window.rows; row++) { - // G_percent(row + 1, window.rows, 2); - G_percent_r(ctx, row); + // G_percent(row, window.rows, 2); + G_progress_update(ctx, row + 1); col = window.cols; if (!offset) { while (col-- > 0) @@ -71,8 +87,8 @@ int write_output_map(char *output, int offset) Rast_put_row(fd_out, cell, CELL_TYPE); } - G_percent(row, window.rows, 2); - G_percent_context_destroy(ctx); + // G_percent(row, window.rows, 2); + G_progress_context_destroy(ctx); G_free(cell); if (offset) diff --git a/raster/r.texture/execute.c b/raster/r.texture/execute.c index aa092088b53..a3ff3a95096 100644 --- a/raster/r.texture/execute.c +++ b/raster/r.texture/execute.c @@ -104,13 +104,13 @@ int execute_texture(CELL **data, struct dimensions *dim, else G_message(_("Calculating %s..."), measure_menu[measure_idx[0]].desc); - GPercentContext *ctx = G_percent_context_create(last_row - first_row, 10); + GProgressContext *ctx = G_progress_context_create(last_row - first_row, 10); #pragma omp parallel private(row, col, i, j, measure, trow) default(shared) { #pragma omp for schedule(static, 1) ordered for (row = first_row; row < last_row; row++) { trow = row % threads; /* Obtain thread row id */ - G_percent_r(ctx, row - first_row + 1); + G_progress_update(ctx, row - first_row + 1); // G_percent(row, nrows, 1); /* initialize the output row */ @@ -167,7 +167,7 @@ int execute_texture(CELL **data, struct dimensions *dim, } } // G_percent(1, 1, 1); - G_percent_context_destroy(ctx); + G_progress_context_destroy(ctx); for (i = 0; i < threads; i++) { for (j = 0; j < n_outputs; j++) From b16c0500e9b7fc54658013ff4012ce8990aed99f Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Tue, 31 Mar 2026 23:29:38 +0200 Subject: [PATCH 14/23] workaround conversion of function pointer to object pointer type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit progress.c: In function ‘G_set_percent_routine’: progress.c:346:19: error: ISO C forbids conversion of function pointer to object pointer type [-Werror=pedantic] 346 | s.user_data = (void *)fn; | ^ progress.c: In function ‘legacy_percent_adapter’: progress.c:1016:23: error: ISO C forbids conversion of object pointer to function pointer type [-Werror=pedantic] 1016 | void (*fn)(int) = (void (*)(int))ud; | ^ cc1: all warnings being treated as errors --- lib/gis/progress.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 164054f2152..de9f2f5d477 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -95,6 +95,7 @@ static telemetry_t g_percent_telemetry; static atomic_bool g_percent_initialized = false; static atomic_bool g_percent_consumer_started = false; static GProgressSink g_percent_sink = {0}; +static int (*g_legacy_percent_routine)(int) = NULL; static GProgressContext *context_create(size_t, size_t, long); static bool telemetry_has_pending_events(telemetry_t *); @@ -335,15 +336,15 @@ void G_set_percent_routine(int (*fn)(int)) // the return value. if (!fn) { // Reset to default behavior + g_legacy_percent_routine = NULL; set_global_sink(NULL); return; } - // Wrap the legacy function pointer in a sink that casts and calls with - // percent + // Route legacy callbacks through a dedicated function-pointer slot. GProgressSink s = {0}; s.on_progress = legacy_percent_adapter; - // Store the function pointer in user_data - s.user_data = (void *)fn; + s.user_data = NULL; + g_legacy_percent_routine = fn; set_global_sink(&s); } @@ -1013,10 +1014,11 @@ static long now_ns(void) // Legacy compatibility: adapter for void (*fn)(int) static void legacy_percent_adapter(const GProgressEvent *e, void *ud) { - void (*fn)(int) = (void (*)(int))ud; + int (*fn)(int) = g_legacy_percent_routine; + (void)ud; if (fn) { int pct = (int)(e->percent); - fn(pct); + (void)fn(pct); } } From 50593a0656cf127152a712ecfa214117cbe065d3 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Tue, 31 Mar 2026 23:39:52 +0200 Subject: [PATCH 15/23] workaround for missing timespec_get progress.c: In function 'now_ns': progress.c:1009:5: error: implicit declaration of function 'timespec_get' [-Wimplicit-function-declaration] 1009 | timespec_get(&ts, TIME_UTC); | ^~~~~~~~~~~~ progress.c:1009:23: error: 'TIME_UTC' undeclared (first use in this function) 1009 | timespec_get(&ts, TIME_UTC); | ^~~~~~~~ progress.c:1009:23: note: each undeclared identifier is reported only once for each function it appears in make[3]: *** [../../include/Make/Compile.make:32: OBJ.x86_64-w64-mingw32/progress.o] Error 1 make[3]: *** Waiting for unfinished jobs.... paths.c: In function 'G_owner': paths.c:220:12: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast] 220 | return (int)pSidOwner; | ^ make[3]: Leaving directory '/d/a/grass/grass/lib/gis' make[3]: Entering directory '/d/a/grass/grass/lib/proj' test -d OBJ.x86_64-w64-mingw32 || mkdir -p OBJ.x86_64-w64-mingw32 --- lib/gis/progress.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index de9f2f5d477..a055a1911b3 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -45,6 +45,9 @@ #include #include #include +#if defined(__MINGW32__) +#include +#endif #define LOG_CAPACITY 1024 #define LOG_MSG_SIZE 128 @@ -1006,9 +1009,21 @@ static bool output_is_silenced(void) /// Returns the current UTC time in nanoseconds. static long now_ns(void) { +#if defined(TIME_UTC) struct timespec ts; timespec_get(&ts, TIME_UTC); return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +#elif defined(CLOCK_REALTIME) + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +#elif defined(__MINGW32__) + struct timeval tv; + gettimeofday(&tv, NULL); + return (long)tv.tv_sec * 1000000000L + (long)tv.tv_usec * 1000L; +#else + return (long)time(NULL) * 1000000000L; +#endif } // Legacy compatibility: adapter for void (*fn)(int) From cf784c33faaae72bf33b57c0af54cfc27cefcb69 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 08:31:32 +0200 Subject: [PATCH 16/23] drop platform specific time guards --- lib/gis/progress.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index a055a1911b3..666cc0e6f44 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -45,9 +45,6 @@ #include #include #include -#if defined(__MINGW32__) -#include -#endif #define LOG_CAPACITY 1024 #define LOG_MSG_SIZE 128 @@ -1017,7 +1014,7 @@ static long now_ns(void) struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; -#elif defined(__MINGW32__) +#elif defined(HAVE_GETTIMEOFDAY) struct timeval tv; gettimeofday(&tv, NULL); return (long)tv.tv_sec * 1000000000L + (long)tv.tv_usec * 1000L; From e95c06e777abf010729504a3c31c17d4b7d20ab2 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 10:57:16 +0200 Subject: [PATCH 17/23] minor fixes --- include/grass/defs/gis.h | 9 +- lib/gis/progress.c | 383 +++++++++++++++++++++++---------------- 2 files changed, 231 insertions(+), 161 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index 37e5fc6f8cf..cefb4e34cc2 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -675,11 +675,10 @@ GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); void G_progress_context_destroy(GProgressContext *); void G_progress_update(GProgressContext *, size_t); -void G_progress_log(GProgressContext *ctx, const char *message); -void G_progress_context_set_sink(GProgressContext *ctx, - const GProgressSink *sink); -void G_progress_increment(GProgressContext *ctx, size_t step); -void G_progress_tick(GProgressContext *ctx); +void G_progress_log(GProgressContext *, const char *); +void G_progress_context_set_sink(GProgressContext *, const GProgressSink *); +void G_progress_increment(GProgressContext *, size_t); +void G_progress_tick(GProgressContext *); #endif // G_USE_PROGRESS_NG /* popen.c */ diff --git a/lib/gis/progress.c b/lib/gis/progress.c index 666cc0e6f44..ffeba700313 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -27,6 +27,10 @@ /// process-wide G_percent() path initializes one shared telemetry instance and /// a detached consumer thread on first use. /// +/// The requirements of the new `G_progress_*` API is support of C11 atomic +/// operations and presens of pthreads, which --if fulfilled-- is indicated by +/// the definition of G_USE_PROGRESS_NG. +/// /// (C) 2026 by the GRASS Development Team /// /// SPDX-License-Identifier: GPL-2.0-or-later @@ -111,14 +115,14 @@ static void context_progress_percent(telemetry_t *, size_t); static void context_progress_time(telemetry_t *, size_t); static void *telemetry_consumer(void *); static void start_global_percent(size_t, size_t); -static void set_global_sink(const GProgressSink *sink); +static void set_global_sink(const GProgressSink *); static bool output_is_silenced(void); static long now_ns(void); -static void legacy_percent_adapter(const GProgressEvent *e, void *ud); -static void sink_progress_standard(const GProgressEvent *e, void *ud); -static void sink_progress_plain(const GProgressEvent *e, void *ud); -static void sink_progress_gui(const GProgressEvent *e, void *ud); -static void sink_log_default(const char *message, void *ud); +static void legacy_percent_adapter(const GProgressEvent *, void *); +static void sink_progress_standard(const GProgressEvent *, void *); +static void sink_progress_plain(const GProgressEvent *, void *); +static void sink_progress_gui(const GProgressEvent *, void *); +static void sink_log_default(const char *, void *); /// Creates an isolated progress-reporting context for concurrent work. /// @@ -321,156 +325,6 @@ void G_progress_log(GProgressContext *ctx, const char *message) telemetry_log(&ctx->telemetry, message); } -// Transitional no-op: retained for compatibility with legacy callers -void G_percent_reset(void) -{ - // No global state to reset in the concurrent API. - // Kept to avoid breaking legacy code paths that call G_percent_reset(). -} - -// Compatibility layer for legacy percent routine API -void G_set_percent_routine(int (*fn)(int)) -{ - // The historical signature in gis.h declares int (*)(int), but actual - // implementers often used void(*)(int). We accept int-returning and ignore - // the return value. - if (!fn) { - // Reset to default behavior - g_legacy_percent_routine = NULL; - set_global_sink(NULL); - return; - } - // Route legacy callbacks through a dedicated function-pointer slot. - GProgressSink s = {0}; - s.on_progress = legacy_percent_adapter; - s.user_data = NULL; - g_legacy_percent_routine = fn; - set_global_sink(&s); -} - -void G_unset_percent_routine(void) -{ - // Reset to default (env-driven G_info_format output) - set_global_sink(NULL); -} - -/// Reports global progress when completion crosses the next percentage step. -/// -/// This function initializes the shared global telemetry stream on first use, -/// clamps `current_element` into the valid `0...total_num_elements` range, and -/// enqueues a progress update only when the computed percentage reaches the -/// next configured threshold. When progress reaches the total, a terminal -/// `100%` event is always queued and the background consumer is asked to stop -/// after pending events have been flushed. -/// -/// \param current_element The current completed element index or count. -/// \param total_num_elements The total number of elements to process. Values -/// less than or equal to `0` disable reporting. -/// \param percent_step The minimum percentage increment required before a new -/// progress event is emitted. -void G_percent(long current_element, long total_num_elements, int percent_step) -{ - if (total_num_elements <= 0 || output_is_silenced()) - return; - - start_global_percent((size_t)total_num_elements, (size_t)percent_step); - - // If someone initialized with different totals/steps, we keep the first - // ones for simplicity. - - size_t total = (size_t)total_num_elements; - size_t completed = (current_element < 0) ? 0 : (size_t)current_element; - if (completed > total) - completed = total; - - if (g_percent_telemetry.percent_step == 0) - return; // not configured - - atomic_store_explicit(&g_percent_telemetry.completed, completed, - memory_order_release); - - if (completed == total) { - telemetry_enqueue_final_progress(&g_percent_telemetry); - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - return; - } - - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = atomic_load_explicit( - &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + g_percent_telemetry.percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &g_percent_telemetry.next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(&g_percent_telemetry, &ev); - if (completed == total) { - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - } - return; - } - // CAS failed; expected updated, loop continues - } -} - -void G_progress(long n, int s) -{ - // Mirror legacy behavior: emit on multiples of s, and handle first tick - // formatting. We route through the global telemetry so it benefits from the - // consumer thread. - - if (s <= 0 || output_is_silenced()) - return; - - // Initialize global telemetry if needed with percent_step=0 to disable - // percent thresholds - start_global_percent(0, 0); - - // Use time-based gating if an interval is configured; otherwise, we emit - // only on multiples of s. Here, we implement the modulo gating explicitly. - - if (n == s && n == 1) { - // For default sink, legacy prints a leading CR/Newline depending on - // format. We simulate this by enqueueing a LOG event when using default - // sink; custom sinks can ignore. - if (g_percent_telemetry.sink.on_progress == NULL) { - switch (g_percent_telemetry.info_format) { - case G_INFO_FORMAT_PLAIN: - telemetry_log(&g_percent_telemetry, "\n"); - break; - case G_INFO_FORMAT_GUI: - // No-op; GUI variant prints on progress events - break; - default: - // STANDARD and others: carriage return - telemetry_log(&g_percent_telemetry, "\r"); - break; - } - } - return; - } - - if (n % s != 0) - return; - - // For counter mode, we do not know total; publish completed=n, total=0 - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = (n < 0 ? 0 : (size_t)n); - ev.total = 0; // unknown total; consumer/sink can render raw counts - enqueue_event(&g_percent_telemetry, &ev); -} - /// Creates and initializes a progress reporting context. /// /// The created context configures its reporting mode based on `step`. When @@ -1083,4 +937,221 @@ static void sink_log_default(const char *message, void *ud) printf("[LOG] %s\n", message); } +// Legacy API + +/// Reports global progress when completion crosses the next percentage step. +/// +/// This routine prints a percentage complete message to stderr. The +/// percentage complete is `(current_element/total_num_elements)\*100`, and +/// these are printed only for each __s__ percentage. This is perhaps best +/// explained by example: +/// +/// ```c +/// #include +/// #include +/// +/// int nrows = 1352; // 1352 is not a special value - example only +/// +/// G_message(_("Percent complete...")); +/// for (int row = 0; row < nrows; row++) +/// { +/// G_percent(row, nrows, 10); +/// do_calculation(row); +/// } +/// G_percent(1, 1, 1); +/// ``` +/// +/// This example code will print completion messages at 10% increments; +/// i.e., 0%, 10%, 20%, 30%, etc., up to 100%. Each message does not appear +/// on a new line, but rather erases the previous message. +/// +/// This function initializes the shared global telemetry stream on first use, +/// clamps `current_element` into the valid `0...total_num_elements` range, and +/// enqueues a progress update only when the computed percentage reaches the +/// next configured threshold. When progress reaches the total, a terminal +/// `100%` event is always queued and the background consumer is asked to stop +/// after pending events have been flushed. +/// +/// \param current_element The current completed element index or count. +/// \param total_num_elements The total number of elements to process. Values +/// less than or equal to `0` disable reporting. +/// \param percent_step The minimum percentage increment required before a new +/// progress event is emitted. +void G_percent(long current_element, long total_num_elements, int percent_step) +{ + if (total_num_elements <= 0 || output_is_silenced()) + return; + + start_global_percent((size_t)total_num_elements, (size_t)percent_step); + + // If someone initialized with different totals/steps, we keep the first + // ones for simplicity. + + size_t total = (size_t)total_num_elements; + size_t completed = (current_element < 0) ? 0 : (size_t)current_element; + if (completed > total) + completed = total; + + if (g_percent_telemetry.percent_step == 0) + return; // not configured + + atomic_store_explicit(&g_percent_telemetry.completed, completed, + memory_order_release); + + if (completed == total) { + telemetry_enqueue_final_progress(&g_percent_telemetry); + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + return; + } + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = atomic_load_explicit( + &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + g_percent_telemetry.percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &g_percent_telemetry.next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(&g_percent_telemetry, &ev); + if (completed == total) { + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + } + return; + } + // CAS failed; expected updated, loop continues + } +} + +/// Reset G_percent() to 0%; do not add newline. +/// +/// \note Transitional no-op: retained for compatibility with legacy callers. +/// +/// \deprecated For new or updated code G_percent_reset() is not needed. +void G_percent_reset(void) +{ + // No global state to reset in the concurrent API. + // Kept to avoid breaking legacy code paths that call G_percent_reset(). +} + +/// Print progress info messages +/// +/// Use G_progress_update(), or G_percent() for legacy code, when number of +/// elements is defined. +/// +/// This routine prints a progress info message to stderr. The value +/// `n` is printed only for each `s`. This is perhaps best +/// explained by example: +/// +/// ```c +/// #include +/// +/// int line = 0; + +/// G_message(_("Reading features...")); +/// while(TRUE) +/// { +/// if (Vect_read_next_line(Map, Points, Cats) < 0) +/// break; +/// line++; +/// G_progress(line, 1e3); +/// } +/// G_progress(1, 1); +/// ``` +/// +/// This example code will print progress in messages at 1000 increments; +/// i.e., 1000, 2000, 3000, 4000, etc., up to number of features for +/// given vector map. Each message does not appear on a new line, but +/// rather erases the previous message. +/// +/// \param n current element +/// \param s increment size +void G_progress(long n, int s) +{ + // Mirror legacy behavior: emit on multiples of s, and handle first tick + // formatting. We route through the global telemetry so it benefits from the + // consumer thread. + + if (s <= 0 || output_is_silenced()) + return; + + // Initialize global telemetry if needed with percent_step=0 to disable + // percent thresholds + start_global_percent(0, 0); + + // Use time-based gating if an interval is configured; otherwise, we emit + // only on multiples of s. Here, we implement the modulo gating explicitly. + + if (n == s && n == 1) { + // For default sink, legacy prints a leading CR/Newline depending on + // format. We simulate this by enqueueing a LOG event when using default + // sink; custom sinks can ignore. + if (g_percent_telemetry.sink.on_progress == NULL) { + switch (g_percent_telemetry.info_format) { + case G_INFO_FORMAT_PLAIN: + telemetry_log(&g_percent_telemetry, "\n"); + break; + case G_INFO_FORMAT_GUI: + // No-op; GUI variant prints on progress events + break; + default: + // STANDARD and others: carriage return + telemetry_log(&g_percent_telemetry, "\n"); + break; + } + } + return; + } + + if (n % s != 0) + return; + + // For counter mode, we do not know total; publish completed=n, total=0 + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = (n < 0 ? 0 : (size_t)n); + ev.total = 0; // unknown total; consumer/sink can render raw counts + enqueue_event(&g_percent_telemetry, &ev); +} + +// Compatibility layer for legacy percent routine API +/// Establishes percent_routine as the routine that will handle the printing of +/// percentage progress messages. +/// +/// \param percent_routine routine will be called like this: percent_routine(x) + +void G_set_percent_routine(int (*fn)(int)) +{ + // The historical signature in gis.h declares int (*)(int), but actual + // implementers often used void(*)(int). We accept int-returning and ignore + // the return value. + if (!fn) { + // Reset to default behavior + g_legacy_percent_routine = NULL; + set_global_sink(NULL); + return; + } + // Route legacy callbacks through a dedicated function-pointer slot. + GProgressSink s = {0}; + s.on_progress = legacy_percent_adapter; + s.user_data = NULL; + g_legacy_percent_routine = fn; + set_global_sink(&s); +} + +void G_unset_percent_routine(void) +{ + // Reset to default (env-driven G_info_format output) + set_global_sink(NULL); +} + #endif // defined(G_USE_PROGRESS_NG) From 41eba455726aeca00759969f07b7c78fadd4e25b Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 11:55:40 +0200 Subject: [PATCH 18/23] move to percent.c --- lib/gis/percent.c | 1125 +++++++++++++++++++++++++++++++++++++++++++- lib/gis/progress.c | 4 + 2 files changed, 1123 insertions(+), 6 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index f66dca7bfc3..46c4d7e110a 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -1,19 +1,67 @@ /*! \file lib/gis/percent.c - \brief GIS Library - percentage progress functions. + \brief GIS Library - Progress reporting and telemetry support for + GRASS operations - (C) 2001-2009, 2011 by the GRASS Development Team + This file implements the `G_progress_*` API used to report incremental work + completion for long-running operations. It supports both percentage-based + and time-based progress contexts, plus compatibility wrappers for the legacy + global G_percent() and G_progress() entry points. - This program is free software under the GNU General Public License - (>=v2). Read the file COPYING that comes with GRASS for details. + The implementation is organized as a telemetry pipeline. Producer-side API + calls update atomic progress state and enqueue ::event_type_t `EV_PROGRESS` + or `EV_LOG` records into a bounded ring buffer. A single consumer thread + drains that buffer, converts raw records into `GProgressEvent` values, and + forwards them either to installed sink callbacks or to default renderers + selected from the current G_info_format() mode. + + Concurrency is designed as multi-producer, single-consumer per telemetry + stream. Producers reserve slots with an atomic `write_index`, publish events + by setting a per-slot `ready` flag with release semantics, and use atomic + compare-and-swap to ensure that only one producer emits a given percent + threshold or time-gated update. The consumer advances a non-atomic + `read_index`, waits for published slots, processes events in FIFO order, and + then marks slots free again. + + Two lifecycle models are used. Isolated `GProgressContext` instances create + a dedicated consumer thread that is joined during destruction. The legacy + process-wide G_percent() path initializes one shared telemetry instance and + a detached consumer thread on first use. + + The requirements of the new `G_progress_*` API is support of C11 atomic + operations and presens of pthreads, which --if are met-- is indicated by + the definition of G_USE_PROGRESS_NG. + + \copyright + (C) 2001-2026 by the GRASS Development Team + \copyright + SPDX-License-Identifier: GPL-2.0-or-later \author GRASS Development Team + \author Nicklas Larsson (concurrency support) */ #include -#if !defined(G_USE_PROGRESS_NG) +#if defined(G_USE_PROGRESS_NG) + +#include +#include +#include +#include +#include +#include +#include +#include + +static void G__percent_ng(long, long, int); +static void G__percent_reset_ng(void); +static void G__progress_ng(long n, int s); +static void G__set_percent_routine_ng(int (*)(int)); +static void G__unset_percent_routine_ng(void); + +#else #include @@ -25,6 +73,8 @@ static struct state { static struct state *st = &state; static int (*ext_percent)(int); +#endif + /*! \brief Print percent complete messages. @@ -63,6 +113,9 @@ static int (*ext_percent)(int); */ void G_percent(long n, long d, int s) { +#if defined(G_USE_PROGRESS_NG) + G__percent_ng(n, d, s); +#else int x, format; format = G_info_format(); @@ -112,6 +165,7 @@ void G_percent(long n, long d, int s) st->prev = -1; st->first = 1; } +#endif } /*! @@ -119,8 +173,12 @@ void G_percent(long n, long d, int s) */ void G_percent_reset(void) { +#if defined(G_USE_PROGRESS_NG) + G__percent_reset_ng(); +#else st->prev = -1; st->first = 1; +#endif } /*! @@ -160,6 +218,9 @@ void G_percent_reset(void) */ void G_progress(long n, int s) { +#if defined(G_USE_PROGRESS_NG) + G__progress_ng(n, s); +#else int format; format = G_info_format(); @@ -184,6 +245,7 @@ void G_progress(long n, int s) else fprintf(stderr, "%10ld\b\b\b\b\b\b\b\b\b\b", n); } +#endif } /*! @@ -194,7 +256,11 @@ void G_progress(long n, int s) */ void G_set_percent_routine(int (*percent_routine)(int)) { +#if defined(G_USE_PROGRESS_NG) + G__set_percent_routine_ng(percent_routine); +#else ext_percent = percent_routine; +#endif } /*! @@ -205,7 +271,1054 @@ void G_set_percent_routine(int (*percent_routine)(int)) */ void G_unset_percent_routine(void) { +#if defined(G_USE_PROGRESS_NG) + G__unset_percent_routine_ng(); +#else ext_percent = NULL; +#endif +} + +#if defined(G_USE_PROGRESS_NG) + +#define LOG_CAPACITY 1024 +#define LOG_MSG_SIZE 128 +#define TIME_RATE_LIMIT_MS 100 + +typedef enum { EV_LOG, EV_PROGRESS } event_type_t; + +typedef struct { + event_type_t type; + size_t completed; + size_t total; + char message[LOG_MSG_SIZE]; + atomic_bool ready; +} event_t; + +/// Internal telemetry state shared by the progress producer and consumer. +/// +/// `telemetry_t` owns the ring buffer of queued log/progress events, the +/// counters and thresholds used for time- or percent-based emission, and the +/// sink configuration that determines how flushed events are rendered. +typedef struct { + event_t buffer[LOG_CAPACITY]; + atomic_size_t write_index; + size_t read_index; + atomic_size_t completed; + size_t total; + int info_format; + atomic_long last_progress_ns; + long interval_ns; + size_t percent_step; + atomic_size_t next_percent_threshold; + atomic_bool stop; + GProgressSink sink; // optional sink +} telemetry_t; + +typedef void (*context_progress_fn)(telemetry_t *, size_t); + +struct GProgressContext { + telemetry_t telemetry; + context_progress_fn report_progress; + GProgressSink sink; // per-context override (optional) + atomic_bool initialized; + pthread_t consumer_thread; + atomic_bool consumer_started; +}; + +static telemetry_t g_percent_telemetry; +static atomic_bool g_percent_initialized = false; +static atomic_bool g_percent_consumer_started = false; +static GProgressSink g_percent_sink = {0}; +static int (*g_legacy_percent_routine)(int) = NULL; + +static GProgressContext *context_create(size_t, size_t, long); +static bool telemetry_has_pending_events(telemetry_t *); +static void telemetry_init_time(telemetry_t *, size_t, long); +static void telemetry_init_percent(telemetry_t *, size_t, size_t); +static void enqueue_event(telemetry_t *, event_t *); +static void telemetry_enqueue_final_progress(telemetry_t *); +static void telemetry_log(telemetry_t *, const char *); +static void telemetry_set_info_format(telemetry_t *); +static void telemetry_install_default_sink(telemetry_t *t); +static void telemetry_progress(telemetry_t *, size_t); +static void context_progress_percent(telemetry_t *, size_t); +static void context_progress_time(telemetry_t *, size_t); +static void *telemetry_consumer(void *); +static void start_global_percent(size_t, size_t); +static void set_global_sink(const GProgressSink *); +static bool output_is_silenced(void); +static long now_ns(void); +static void legacy_percent_adapter(const GProgressEvent *, void *); +static void sink_progress_standard(const GProgressEvent *, void *); +static void sink_progress_plain(const GProgressEvent *, void *); +static void sink_progress_gui(const GProgressEvent *, void *); +static void sink_log_default(const char *, void *); + +/// Creates an isolated progress-reporting context for concurrent work. +/// +/// The returned context tracks progress for `total_num_elements` items and +/// emits progress updates whenever completion advances by at least +/// `percent_step` percentage points. `total_num_elements` must match the +/// actual number of work units that will be reported through +/// `G_progress_update()`. In particular, callers should pass a completed-work +/// count, not a raw loop index or a larger container size, otherwise the +/// terminal `100%` update may never be reached. If output is enabled by the +/// current runtime configuration, this function also starts the background +/// consumer thread used to flush queued telemetry events. +/// +/// \param total_num_elements Total number of elements to process. +/// \param step Minimum percentage increment that triggers a +/// progress event. +/// \return A newly allocated `GPercentContext`, or `NULL` if output +/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or +/// verbosity level is below `1`. +GProgressContext *G_progress_context_create(size_t total_num_elements, + size_t step) +{ + return context_create(total_num_elements, step, + (step == 0 ? TIME_RATE_LIMIT_MS : 0)); +} + +/// Creates an isolated progress-reporting context with time-based updates. +/// +/// Unlike `G_progress_context_create()`, which emits progress events when +/// completion crosses percentage thresholds, this variant rate-limits progress +/// emission by elapsed time. The returned context tracks +/// `total_num_elements` work units and reports updates no more frequently than +/// once every `interval_ms` milliseconds while work is in progress. +/// +/// Callers should report monotonically increasing completed-work counts through +/// `G_progress_update()` and destroy the context with +/// `G_progress_context_destroy()` when processing finishes. +/// +/// \param total_num_elements Total number of elements to process. +/// \param interval_ms Minimum time interval, in milliseconds, between emitted +/// progress updates. +/// \return A newly allocated `GProgressContext`, or `NULL` if output is +/// silenced by the current runtime configuration. +GProgressContext *G_progress_context_create_time(size_t total_num_elements, + long interval_ms) +{ + return context_create(total_num_elements, 0, interval_ms); +} + +/// Destroys a `GPercentContext` and releases any resources it owns. +/// +/// This function stops the context's background telemetry consumer, waits for +/// the consumer thread to finish when it was started, marks the context as no +/// longer initialized, and frees the context memory. Passing `NULL` is safe and +/// has no effect. +/// +/// \param ctx The progress-reporting context previously created by +/// `G_percent_context_create()`, or `NULL`. +void G_progress_context_destroy(GProgressContext *ctx) +{ + if (!ctx) { + return; + } + + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { + G_free(ctx); + return; + } + + if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= + ctx->telemetry.total && + atomic_load_explicit(&ctx->telemetry.next_percent_threshold, + memory_order_acquire) <= 100) { + telemetry_enqueue_final_progress(&ctx->telemetry); + } + + atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); + + if (atomic_exchange_explicit(&ctx->consumer_started, false, + memory_order_acq_rel)) { + pthread_join(ctx->consumer_thread, NULL); + } + + atomic_store_explicit(&ctx->initialized, false, memory_order_release); + G_free(ctx); +} + +/// Sets or clears the output sink used by a progress context. +/// +/// Installs a per-context `GProgressSink` override for progress and log events +/// emitted by `ctx`. When `sink` is non-`NULL`, its callbacks and `user_data` +/// are copied into the context and used by the telemetry consumer. Passing +/// `NULL` clears any custom sink so the context falls back to its default +/// output behavior. +/// +/// \param ctx The progress context to update. If `NULL`, the function has +/// no effect. +/// \param sink The sink configuration to copy into the context, or `NULL` +/// to remove the custom sink. +/// +/// Example: +/// ```c +/// GProgressSink sink = { +/// .on_progress = my_progress_handler, +/// .on_log = my_log_handler, +/// .user_data = my_context, +/// }; +/// G_progress_context_set_sink(progress_ctx, &sink); +/// ``` +void G_progress_context_set_sink(GProgressContext *ctx, + const GProgressSink *sink) +{ + if (!ctx) + return; + if (sink) { + ctx->sink = *sink; + } + else { + ctx->sink.on_progress = NULL; + ctx->sink.on_log = NULL; + ctx->sink.user_data = NULL; + } + // update telemetry copy; safe because sink is read-only + // by consumer after set + ctx->telemetry.sink = ctx->sink; +} + +/// Reports progress for an isolated `GProgressContext` instance. +/// +/// This re-entrant variant of `G_percent` is intended for concurrent or +/// context-specific work. It validates that `ctx` is initialized, clamps +/// `current_element` to the valid `0...total` range, and enqueues a progress +/// event only when the computed percentage reaches the next configured +/// threshold for the context. +/// +/// Callers typically create the context with `G_progress_context_create()`, +/// call this function as work advances, and later release resources with +/// `G_progress_context_destroy()`. +/// +/// Example: +/// ```c +/// size_t n_rows = window.rows; // total number of rows +/// size_t step = 10; // output step, every 10% +/// GProgressContext *ctx = G_progress_context_create(n_rows, step); +/// for (row = 0; row < window.rows; row++) { +/// // costly calculation ... +/// +/// // note: not counting from zero, as for loop never reaches n_rows +/// // and we want to reach 100% +/// size_t completed_row = row + 1; +/// +/// G_progress_update(ctx, completed_row); +/// } +/// G_progress_context_destroy(ctx); +/// ``` +/// +/// \param ctx The progress-reporting context created by +/// `G_percent_context_create()`. +/// \param completed: The current completed element index or count. +void G_progress_update(GProgressContext *ctx, size_t completed) +{ + if (!ctx) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + + telemetry_t *t = &ctx->telemetry; + if (t->total == 0) + return; + + size_t total = t->total; + if (completed > total) + completed = total; + + atomic_store_explicit(&t->completed, completed, memory_order_release); + ctx->report_progress(t, completed); +} + +void G_progress_increment(GProgressContext *ctx, size_t step) +{ + if (!ctx || step == 0) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + + telemetry_progress(&ctx->telemetry, step); +} + +void G_progress_tick(GProgressContext *ctx) +{ + G_progress_increment(ctx, 1); +} + +void G_progress_log(GProgressContext *ctx, const char *message) +{ + if (!ctx || !message) + return; + if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) + return; + telemetry_log(&ctx->telemetry, message); +} + +/// Creates and initializes a progress reporting context. +/// +/// The created context configures its reporting mode based on `step`. When +/// `step` is `0`, progress updates are emitted using a time-based interval +/// controlled by `interval_ms`. Otherwise, progress updates are emitted at +/// percentage increments defined by `step`. +/// +/// \param total_num_elements Total number of elements expected for the +/// operation being tracked. +/// \param step Percentage increment for reporting progress. A value of `0` +/// selects time-based reporting instead. +/// \param interval_ms Time interval in milliseconds between progress updates +/// when `step` is `0`. +/// \return A newly allocated and initialized `GProgressContext`, or `NULL` +/// if output is currently silenced. +static GProgressContext *context_create(size_t total_num_elements, size_t step, + long interval_ms) +{ + if (output_is_silenced()) + return NULL; + + GProgressContext *ctx = G_calloc(1, sizeof(*ctx)); + + atomic_init(&ctx->initialized, true); + + assert(step <= 100); + + if (step == 0) { + assert(interval_ms > 0); + telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); + ctx->report_progress = context_progress_time; + } + else { + telemetry_init_percent(&ctx->telemetry, total_num_elements, step); + ctx->report_progress = context_progress_percent; + } + + ctx->sink.on_progress = NULL; + ctx->sink.on_log = NULL; + ctx->sink.user_data = NULL; + + // propagate context sink to telemetry by default + ctx->telemetry.sink = ctx->sink; + + atomic_init(&ctx->consumer_started, false); + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &ctx->consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, + &ctx->telemetry); + } + + return ctx; +} + +static void context_progress_percent(telemetry_t *t, size_t completed) +{ + size_t total = t->total; + + if (completed == total) { + telemetry_enqueue_final_progress(t); + return; + } + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = + atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(t, &ev); + return; + } + } +} + +static void context_progress_time(telemetry_t *t, size_t completed) +{ + if (completed == t->total) { + telemetry_enqueue_final_progress(t); + return; + } + + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, + now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = t->total; + enqueue_event(t, &ev); +} + +/// Consumes queued telemetry events and emits log or progress output until +/// shutdown is requested and the event buffer has been drained. +/// +/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and +/// formatting settings should be consumed. +/// \return `NULL` after the consumer loop exits and any global consumer state +/// has been reset. +static void *telemetry_consumer(void *arg) +{ + telemetry_t *t = arg; + + while (true) { + if (atomic_load_explicit(&t->stop, memory_order_acquire) && + !telemetry_has_pending_events(t)) { + break; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + + if (!atomic_load_explicit(&ev->ready, memory_order_acquire)) { + sched_yield(); + continue; + } + + // handle event + if (ev->type == EV_LOG) { + if (t->sink.on_log) { + t->sink.on_log(ev->message, t->sink.user_data); + } + else { + sink_log_default(ev->message, NULL); + } + } + else if (ev->type == EV_PROGRESS) { + double pct = (ev->total > 0) + ? (double)ev->completed * 100.0 / (double)ev->total + : 0.0; + bool is_terminal = (ev->total > 0 && ev->completed >= ev->total); + + if (t->sink.on_progress) { + GProgressEvent pe = { + .completed = ev->completed, + .total = ev->total, + .percent = pct, + .is_terminal = is_terminal, + }; + t->sink.on_progress(&pe, t->sink.user_data); + } + else { + // Ensure defaults exist (defensive, should already be set at + // init) + telemetry_install_default_sink(t); + if (t->sink.on_progress) { + GProgressEvent pe = { + .completed = ev->completed, + .total = ev->total, + .percent = pct, + .is_terminal = is_terminal, + }; + t->sink.on_progress(&pe, t->sink.user_data); + } + } + } + + // mark slot free + atomic_store_explicit(&ev->ready, false, memory_order_release); + t->read_index++; + } + + if (t == &g_percent_telemetry) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + // keep g_percent_sink as-is; no change needed on shutdown + } + + return NULL; +} + +static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = interval_ms * 1000000L; + + t->percent_step = 0; // 0 => disabled, use time-based if interval_ns > 0 + atomic_init(&t->next_percent_threshold, 0); + + atomic_init(&t->stop, false); + + // default: no custom sink; callbacks NULL imply fallback to info_format + t->sink.on_progress = NULL; + t->sink.on_log = NULL; + t->sink.user_data = NULL; + telemetry_install_default_sink(t); +} + +/// Initializes telemetry state for percentage-based progress reporting. +/// +/// Resets the telemetry ring buffer and counters, disables time-based +/// throttling, and configures the next progress event to be emitted when the +/// completed work reaches the first `percent_step` threshold. +/// +/// \param t The telemetry instance to reset and configure. +/// \param total The total number of work units expected for the tracked +/// operation. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. A value of `0` disables percentage-based +/// thresholds. +static void telemetry_init_percent(telemetry_t *t, size_t total, + size_t percent_step) +{ + atomic_init(&t->write_index, 0); + t->read_index = 0; + for (size_t i = 0; i < LOG_CAPACITY; ++i) { + atomic_init(&t->buffer[i].ready, false); + } + atomic_init(&t->completed, 0); + t->total = total; + telemetry_set_info_format(t); + + // disable time-based gating + atomic_init(&t->last_progress_ns, 0); + t->interval_ns = 0; + + // enable percentage-based gating + t->percent_step = percent_step; + size_t first = percent_step > 0 ? percent_step : 0; + atomic_init(&t->next_percent_threshold, first); + + atomic_init(&t->stop, false); + + // default: no custom sink; callbacks NULL imply fallback to info_format + t->sink.on_progress = NULL; + t->sink.on_log = NULL; + t->sink.user_data = NULL; + telemetry_install_default_sink(t); +} + +/// Queues a telemetry event into the ring buffer for later consumption. +/// +/// Waits until the destination slot becomes available, copies the event payload +/// into that slot, and then marks the slot as ready using release semantics so +/// readers can safely observe the published event. +/// +/// \param t The telemetry instance that owns the event buffer. +/// \param src The event payload to enqueue. +static void enqueue_event(telemetry_t *t, event_t *src) +{ + size_t idx = + atomic_fetch_add_explicit(&t->write_index, 1, memory_order_relaxed); + + event_t *dst = &t->buffer[idx % LOG_CAPACITY]; + + // wait until slot is free (bounded spin) + while (atomic_load_explicit(&dst->ready, memory_order_acquire)) { + sched_yield(); + } + + // copy payload + *dst = *src; + + // publish + atomic_store_explicit(&dst->ready, true, memory_order_release); +} + +/// Queues a terminal `100%` progress event for a telemetry stream. +/// +/// This helper records the stream as fully completed, disables further +/// percentage-threshold reporting, and enqueues one last progress event with +/// `completed == total` so the consumer can emit the final `100%` update. +/// +/// \param t The telemetry instance to finalize. +static void telemetry_enqueue_final_progress(telemetry_t *t) +{ + event_t ev = {0}; + + atomic_store_explicit(&t->completed, t->total, memory_order_release); + atomic_store_explicit(&t->next_percent_threshold, 101, + memory_order_release); + + ev.type = EV_PROGRESS; + ev.completed = t->total; + ev.total = t->total; + enqueue_event(t, &ev); +} + +static bool telemetry_has_pending_events(telemetry_t *t) +{ + if (t->read_index != + atomic_load_explicit(&t->write_index, memory_order_acquire)) { + return true; + } + + event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; + return atomic_load_explicit(&ev->ready, memory_order_acquire); +} + +static void telemetry_log(telemetry_t *t, const char *msg) +{ + event_t ev = {0}; + ev.type = EV_LOG; + snprintf(ev.message, LOG_MSG_SIZE, "%s", msg); + + enqueue_event(t, &ev); +} + +/// Captures the current GRASS info output format for subsequent telemetry. +/// +/// Reads the process-wide info formatting mode and stores it on the telemetry +/// instance so later progress and log events can format output consistently. +/// +/// \param t The telemetry state that caches the active info format. +static void telemetry_set_info_format(telemetry_t *t) +{ + t->info_format = G_info_format(); +} + +/// Records completed work and enqueues a progress event when the next +/// reportable threshold is reached. +/// +/// The function atomically increments the telemetry's completed counter by +/// `step`, then decides whether to emit a progress event using one of two +/// modes: percent-based reporting when `percent_step` and `total` are +/// configured, or time-based throttling when they are not. Atomic +/// compare-and-swap operations ensure that only one caller emits an event for a +/// given threshold or interval. +/// +/// \param t The telemetry state to update and publish through. +/// \param step The number of newly completed units of work to add. +static void telemetry_progress(telemetry_t *t, size_t step) +{ + size_t new_completed = + atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + + step; + + if (t->percent_step > 0 && t->total > 0) { + if (new_completed >= t->total) { + telemetry_enqueue_final_progress(t); + return; + } + + size_t current_pct = (size_t)((new_completed * 100) / t->total); + size_t expected = atomic_load_explicit(&t->next_percent_threshold, + memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + t->percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; // sentinel beyond 100 to stop further emits + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + // we won the right to emit at this threshold + break; + } + // CAS failed, expected now contains the latest value; loop to + // re-check + } + // If we didn't advance, nothing to emit + if (current_pct < expected || expected > 100) { + return; + } + } + else { + long now = now_ns(); + long last = + atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); + if (now - last < t->interval_ns) { + return; + } + if (!atomic_compare_exchange_strong_explicit( + &t->last_progress_ns, &last, now, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + } + + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = new_completed; + ev.total = t->total; + + enqueue_event(t, &ev); +} + +static void telemetry_install_default_sink(telemetry_t *t) +{ + // Only set defaults if no custom sink is present + if (t->sink.on_progress || t->sink.on_log) + return; + + switch (t->info_format) { + case G_INFO_FORMAT_STANDARD: + t->sink.on_progress = sink_progress_standard; + break; + case G_INFO_FORMAT_GUI: + t->sink.on_progress = sink_progress_gui; + break; + case G_INFO_FORMAT_PLAIN: + t->sink.on_progress = sink_progress_plain; + break; + default: + t->sink.on_progress = NULL; // silent/no output + break; + } + t->sink.on_log = sink_log_default; + t->sink.user_data = NULL; +} + +/// Initializes shared percent-based telemetry and starts the detached consumer +/// thread once. +/// +/// This function performs one-time global setup for percent progress reporting. +/// Repeated calls return immediately after the initialization state has been +/// set. If output is disabled or the consumer thread cannot be created, no +/// further progress consumer setup is performed. +/// +/// \param total_num_elements The total number of elements used to compute +/// progress percentages. +/// \param percent_step The percentage increment that controls when +/// progress updates are emitted. +static void start_global_percent(size_t total_num_elements, size_t percent_step) +{ + bool expected_init = false; + if (!atomic_compare_exchange_strong_explicit( + &g_percent_initialized, &expected_init, true, memory_order_acq_rel, + memory_order_relaxed)) { + return; + } + + telemetry_init_percent(&g_percent_telemetry, + ((total_num_elements > 0) ? total_num_elements : 0), + ((percent_step > 0) ? percent_step : 0)); + + // attach current global sink (may be empty for default behavior) + if (g_percent_sink.on_progress || g_percent_sink.on_log) { + g_percent_telemetry.sink = g_percent_sink; + } // else keep defaults installed by telemetry_init_percent + + bool expected_started = false; + if (atomic_compare_exchange_strong_explicit( + &g_percent_consumer_started, &expected_started, true, + memory_order_acq_rel, memory_order_relaxed)) { + pthread_t consumer_thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&consumer_thread, &attr, telemetry_consumer, + &g_percent_telemetry) != 0) { + atomic_store_explicit(&g_percent_consumer_started, false, + memory_order_release); + atomic_store_explicit(&g_percent_initialized, false, + memory_order_release); + } + pthread_attr_destroy(&attr); + } +} + +/// Sets or clears the global sink used by `G_percent` progress reporting. +/// +/// Copies `sink` into the shared global progress configuration used by the +/// legacy `G_percent` API. When `sink` is non-`NULL`, its callbacks and +/// `user_data` are used for subsequent progress and log events. Passing `NULL` +/// clears the custom sink and restores the default output behavior derived from +/// the current runtime info format. +/// +/// If global progress telemetry has already been initialized, the active +/// telemetry sink is updated immediately so later events follow the new +/// configuration. +/// +/// \param sink The sink configuration to install globally, or `NULL` to remove +/// the custom sink and fall back to the default renderer. +static void set_global_sink(const GProgressSink *sink) +{ + if (sink) { + g_percent_sink = *sink; + } + else { + g_percent_sink.on_progress = NULL; + g_percent_sink.on_log = NULL; + g_percent_sink.user_data = NULL; + } + + // apply to global telemetry if initialized + if (atomic_load_explicit(&g_percent_initialized, memory_order_acquire)) { + if (g_percent_sink.on_progress || g_percent_sink.on_log) { + g_percent_telemetry.sink = g_percent_sink; + } + else { + // reinstall defaults based on current info_format + g_percent_telemetry.sink.on_progress = NULL; + g_percent_telemetry.sink.on_log = NULL; + g_percent_telemetry.sink.user_data = NULL; + telemetry_install_default_sink(&g_percent_telemetry); + } + } +} + +static bool output_is_silenced(void) +{ + return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); +} + +/// Returns the current UTC time in nanoseconds. +static long now_ns(void) +{ +#if defined(TIME_UTC) + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +#elif defined(CLOCK_REALTIME) + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; +#elif defined(HAVE_GETTIMEOFDAY) + struct timeval tv; + gettimeofday(&tv, NULL); + return (long)tv.tv_sec * 1000000000L + (long)tv.tv_usec * 1000L; +#else + return (long)time(NULL) * 1000000000L; +#endif +} + +// Legacy compatibility: adapter for void (*fn)(int) +static void legacy_percent_adapter(const GProgressEvent *e, void *ud) +{ + int (*fn)(int) = g_legacy_percent_routine; + (void)ud; + if (fn) { + int pct = (int)(e->percent); + (void)fn(pct); + } +} + +// Internal default sinks for different G_info_format modes +static void sink_progress_standard(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", e->completed); + return; + } + int pct = (int)(e->percent); + fprintf(stderr, "%4d%%\b\b\b\b\b", pct); + if (pct == 100) + fprintf(stderr, "\n"); +} + +static void sink_progress_plain(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "%zu..", e->completed); + return; + } + int pct = (int)(e->percent); + fprintf(stderr, "%d%s", pct, (pct == 100 ? "\n" : "..")); +} + +static void sink_progress_gui(const GProgressEvent *e, void *ud) +{ + (void)ud; + if (e->total == 0) { + fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", e->completed); + fflush(stderr); + return; + } + int pct = (int)(e->percent); + + int comp = (int)e->completed; + int tot = (int)e->total; + + fprintf(stderr, "GRASS_INFO_PERCENT: %d (%d/%d)\n", pct, comp, tot); + fflush(stderr); +} + +static void sink_log_default(const char *message, void *ud) +{ + (void)ud; + // default logging to stdout + printf("[LOG] %s\n", message); +} + +// Legacy API + +/// Reports global progress when completion crosses the next percentage step. +/// +/// This function initializes the shared global telemetry stream on first use, +/// clamps `current_element` into the valid `0...total_num_elements` range, and +/// enqueues a progress update only when the computed percentage reaches the +/// next configured threshold. When progress reaches the total, a terminal +/// `100%` event is always queued and the background consumer is asked to stop +/// after pending events have been flushed. +/// +/// \param current_element The current completed element index or count. +/// \param total_num_elements The total number of elements to process. Values +/// less than or equal to `0` disable reporting. +/// \param percent_step The minimum percentage increment required before a new +/// progress event is emitted. +static void G__percent_ng(long current_element, long total_num_elements, + int percent_step) +{ + if (total_num_elements <= 0 || output_is_silenced()) + return; + + start_global_percent((size_t)total_num_elements, (size_t)percent_step); + + // If someone initialized with different totals/steps, we keep the first + // ones for simplicity. + + size_t total = (size_t)total_num_elements; + size_t completed = (current_element < 0) ? 0 : (size_t)current_element; + if (completed > total) + completed = total; + + if (g_percent_telemetry.percent_step == 0) + return; // not configured + + atomic_store_explicit(&g_percent_telemetry.completed, completed, + memory_order_release); + + if (completed == total) { + telemetry_enqueue_final_progress(&g_percent_telemetry); + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + return; + } + + size_t current_pct = (size_t)((completed * 100) / total); + size_t expected = atomic_load_explicit( + &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); + while (current_pct >= expected && expected <= 100) { + size_t next = expected + g_percent_telemetry.percent_step; + if (expected < 100 && next > 100) + next = 100; + else if (next > 100) + next = 101; + if (atomic_compare_exchange_strong_explicit( + &g_percent_telemetry.next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = completed; + ev.total = total; + enqueue_event(&g_percent_telemetry, &ev); + if (completed == total) { + atomic_store_explicit(&g_percent_telemetry.stop, true, + memory_order_release); + } + return; + } + // CAS failed; expected updated, loop continues + } +} + +// no-op: retained for compatibility with legacy callers. +static void G__percent_reset_ng(void) +{ + // No global state to reset in the concurrent API. + // Kept to avoid breaking legacy code paths that call G_percent_reset(). +} + +// Print progress info messages +static void G__progress_ng(long n, int s) +{ + // Mirror legacy behavior: emit on multiples of s, and handle first tick + // formatting. We route through the global telemetry so it benefits from the + // consumer thread. + + if (s <= 0 || output_is_silenced()) + return; + + // Initialize global telemetry if needed with percent_step=0 to disable + // percent thresholds + start_global_percent(0, 0); + + // Use time-based gating if an interval is configured; otherwise, we emit + // only on multiples of s. Here, we implement the modulo gating explicitly. + + if (n == s && n == 1) { + // For default sink, legacy prints a leading CR/Newline depending on + // format. We simulate this by enqueueing a LOG event when using default + // sink; custom sinks can ignore. + if (g_percent_telemetry.sink.on_progress == NULL) { + switch (g_percent_telemetry.info_format) { + case G_INFO_FORMAT_PLAIN: + telemetry_log(&g_percent_telemetry, "\n"); + break; + case G_INFO_FORMAT_GUI: + // No-op; GUI variant prints on progress events + break; + default: + // STANDARD and others: carriage return + telemetry_log(&g_percent_telemetry, "\n"); + break; + } + } + return; + } + + if (n % s != 0) + return; + + // For counter mode, we do not know total; publish completed=n, total=0 + event_t ev = {0}; + ev.type = EV_PROGRESS; + ev.completed = (n < 0 ? 0 : (size_t)n); + ev.total = 0; // unknown total; consumer/sink can render raw counts + enqueue_event(&g_percent_telemetry, &ev); +} + +// Compatibility layer for legacy percent routine API +static void G__set_percent_routine_ng(int (*fn)(int)) +{ + // The historical signature in gis.h declares int (*)(int), but actual + // implementers often used void(*)(int). We accept int-returning and ignore + // the return value. + if (!fn) { + // Reset to default behavior + g_legacy_percent_routine = NULL; + set_global_sink(NULL); + return; + } + // Route legacy callbacks through a dedicated function-pointer slot. + GProgressSink s = {0}; + s.on_progress = legacy_percent_adapter; + s.user_data = NULL; + g_legacy_percent_routine = fn; + set_global_sink(&s); +} + +static void G__unset_percent_routine_ng(void) +{ + // Reset to default (env-driven G_info_format output) + set_global_sink(NULL); } -#endif // !defined(G_USE_PROGRESS_NG) +#endif // defined(G_USE_PROGRESS_NG) diff --git a/lib/gis/progress.c b/lib/gis/progress.c index ffeba700313..908bdb0f21a 100644 --- a/lib/gis/progress.c +++ b/lib/gis/progress.c @@ -37,6 +37,8 @@ /// /// \author Nicklas Larsson +#if 0 + #include #if defined(G_USE_PROGRESS_NG) @@ -1155,3 +1157,5 @@ void G_unset_percent_routine(void) } #endif // defined(G_USE_PROGRESS_NG) + +#endif // 0 From 1638c5d3daf1623866bdfa1eeaec5727630bbd73 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 12:06:28 +0200 Subject: [PATCH 19/23] rm progress.c --- lib/gis/progress.c | 1161 -------------------------------------------- 1 file changed, 1161 deletions(-) delete mode 100644 lib/gis/progress.c diff --git a/lib/gis/progress.c b/lib/gis/progress.c deleted file mode 100644 index 908bdb0f21a..00000000000 --- a/lib/gis/progress.c +++ /dev/null @@ -1,1161 +0,0 @@ -/// \file lib/gis/progress.c -/// -/// Progress reporting and telemetry support for GRASS GIS operations. -/// -/// This file implements the `G_progress_*` API used to report incremental work -/// completion for long-running operations. It supports both percentage-based -/// and time-based progress contexts, plus compatibility wrappers for the legacy -/// global G_percent() and G_progress() entry points. -/// -/// The implementation is organized as a telemetry pipeline. Producer-side API -/// calls update atomic progress state and enqueue ::event_type_t `EV_PROGRESS` -/// or `EV_LOG` records into a bounded ring buffer. A single consumer thread -/// drains that buffer, converts raw records into `GProgressEvent` values, and -/// forwards them either to installed sink callbacks or to default renderers -/// selected from the current G_info_format() mode. -/// -/// Concurrency is designed as multi-producer, single-consumer per telemetry -/// stream. Producers reserve slots with an atomic `write_index`, publish events -/// by setting a per-slot `ready` flag with release semantics, and use atomic -/// compare-and-swap to ensure that only one producer emits a given percent -/// threshold or time-gated update. The consumer advances a non-atomic -/// `read_index`, waits for published slots, processes events in FIFO order, and -/// then marks slots free again. -/// -/// Two lifecycle models are used. Isolated `GProgressContext` instances create -/// a dedicated consumer thread that is joined during destruction. The legacy -/// process-wide G_percent() path initializes one shared telemetry instance and -/// a detached consumer thread on first use. -/// -/// The requirements of the new `G_progress_*` API is support of C11 atomic -/// operations and presens of pthreads, which --if fulfilled-- is indicated by -/// the definition of G_USE_PROGRESS_NG. -/// -/// (C) 2026 by the GRASS Development Team -/// -/// SPDX-License-Identifier: GPL-2.0-or-later -/// -/// \author Nicklas Larsson - -#if 0 - -#include - -#if defined(G_USE_PROGRESS_NG) - -#include -#include -#include -#include -#include -#include -#include -#include - -#define LOG_CAPACITY 1024 -#define LOG_MSG_SIZE 128 -#define TIME_RATE_LIMIT_MS 100 - -typedef enum { EV_LOG, EV_PROGRESS } event_type_t; - -typedef struct { - event_type_t type; - size_t completed; - size_t total; - char message[LOG_MSG_SIZE]; - atomic_bool ready; -} event_t; - -/// Internal telemetry state shared by the progress producer and consumer. -/// -/// `telemetry_t` owns the ring buffer of queued log/progress events, the -/// counters and thresholds used for time- or percent-based emission, and the -/// sink configuration that determines how flushed events are rendered. -typedef struct { - event_t buffer[LOG_CAPACITY]; - atomic_size_t write_index; - size_t read_index; - atomic_size_t completed; - size_t total; - int info_format; - atomic_long last_progress_ns; - long interval_ns; - size_t percent_step; - atomic_size_t next_percent_threshold; - atomic_bool stop; - GProgressSink sink; // optional sink -} telemetry_t; - -typedef void (*context_progress_fn)(telemetry_t *, size_t); - -struct GProgressContext { - telemetry_t telemetry; - context_progress_fn report_progress; - GProgressSink sink; // per-context override (optional) - atomic_bool initialized; - pthread_t consumer_thread; - atomic_bool consumer_started; -}; - -static telemetry_t g_percent_telemetry; -static atomic_bool g_percent_initialized = false; -static atomic_bool g_percent_consumer_started = false; -static GProgressSink g_percent_sink = {0}; -static int (*g_legacy_percent_routine)(int) = NULL; - -static GProgressContext *context_create(size_t, size_t, long); -static bool telemetry_has_pending_events(telemetry_t *); -static void telemetry_init_time(telemetry_t *, size_t, long); -static void telemetry_init_percent(telemetry_t *, size_t, size_t); -static void enqueue_event(telemetry_t *, event_t *); -static void telemetry_enqueue_final_progress(telemetry_t *); -static void telemetry_log(telemetry_t *, const char *); -static void telemetry_set_info_format(telemetry_t *); -static void telemetry_install_default_sink(telemetry_t *t); -static void telemetry_progress(telemetry_t *, size_t); -static void context_progress_percent(telemetry_t *, size_t); -static void context_progress_time(telemetry_t *, size_t); -static void *telemetry_consumer(void *); -static void start_global_percent(size_t, size_t); -static void set_global_sink(const GProgressSink *); -static bool output_is_silenced(void); -static long now_ns(void); -static void legacy_percent_adapter(const GProgressEvent *, void *); -static void sink_progress_standard(const GProgressEvent *, void *); -static void sink_progress_plain(const GProgressEvent *, void *); -static void sink_progress_gui(const GProgressEvent *, void *); -static void sink_log_default(const char *, void *); - -/// Creates an isolated progress-reporting context for concurrent work. -/// -/// The returned context tracks progress for `total_num_elements` items and -/// emits progress updates whenever completion advances by at least -/// `percent_step` percentage points. `total_num_elements` must match the -/// actual number of work units that will be reported through -/// `G_progress_update()`. In particular, callers should pass a completed-work -/// count, not a raw loop index or a larger container size, otherwise the -/// terminal `100%` update may never be reached. If output is enabled by the -/// current runtime configuration, this function also starts the background -/// consumer thread used to flush queued telemetry events. -/// -/// \param total_num_elements Total number of elements to process. -/// \param step Minimum percentage increment that triggers a -/// progress event. -/// \return A newly allocated `GPercentContext`, or `NULL` if output -/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or -/// verbosity level is below `1`. -GProgressContext *G_progress_context_create(size_t total_num_elements, - size_t step) -{ - return context_create(total_num_elements, step, - (step == 0 ? TIME_RATE_LIMIT_MS : 0)); -} - -/// Creates an isolated progress-reporting context with time-based updates. -/// -/// Unlike `G_progress_context_create()`, which emits progress events when -/// completion crosses percentage thresholds, this variant rate-limits progress -/// emission by elapsed time. The returned context tracks -/// `total_num_elements` work units and reports updates no more frequently than -/// once every `interval_ms` milliseconds while work is in progress. -/// -/// Callers should report monotonically increasing completed-work counts through -/// `G_progress_update()` and destroy the context with -/// `G_progress_context_destroy()` when processing finishes. -/// -/// \param total_num_elements Total number of elements to process. -/// \param interval_ms Minimum time interval, in milliseconds, between emitted -/// progress updates. -/// \return A newly allocated `GProgressContext`, or `NULL` if output is -/// silenced by the current runtime configuration. -GProgressContext *G_progress_context_create_time(size_t total_num_elements, - long interval_ms) -{ - return context_create(total_num_elements, 0, interval_ms); -} - -/// Destroys a `GPercentContext` and releases any resources it owns. -/// -/// This function stops the context's background telemetry consumer, waits for -/// the consumer thread to finish when it was started, marks the context as no -/// longer initialized, and frees the context memory. Passing `NULL` is safe and -/// has no effect. -/// -/// \param ctx The progress-reporting context previously created by -/// `G_percent_context_create()`, or `NULL`. -void G_progress_context_destroy(GProgressContext *ctx) -{ - if (!ctx) { - return; - } - - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) { - G_free(ctx); - return; - } - - if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= - ctx->telemetry.total && - atomic_load_explicit(&ctx->telemetry.next_percent_threshold, - memory_order_acquire) <= 100) { - telemetry_enqueue_final_progress(&ctx->telemetry); - } - - atomic_store_explicit(&ctx->telemetry.stop, true, memory_order_release); - - if (atomic_exchange_explicit(&ctx->consumer_started, false, - memory_order_acq_rel)) { - pthread_join(ctx->consumer_thread, NULL); - } - - atomic_store_explicit(&ctx->initialized, false, memory_order_release); - G_free(ctx); -} - -/// Sets or clears the output sink used by a progress context. -/// -/// Installs a per-context `GProgressSink` override for progress and log events -/// emitted by `ctx`. When `sink` is non-`NULL`, its callbacks and `user_data` -/// are copied into the context and used by the telemetry consumer. Passing -/// `NULL` clears any custom sink so the context falls back to its default -/// output behavior. -/// -/// \param ctx The progress context to update. If `NULL`, the function has -/// no effect. -/// \param sink The sink configuration to copy into the context, or `NULL` -/// to remove the custom sink. -/// -/// Example: -/// ```c -/// GProgressSink sink = { -/// .on_progress = my_progress_handler, -/// .on_log = my_log_handler, -/// .user_data = my_context, -/// }; -/// G_progress_context_set_sink(progress_ctx, &sink); -/// ``` -void G_progress_context_set_sink(GProgressContext *ctx, - const GProgressSink *sink) -{ - if (!ctx) - return; - if (sink) { - ctx->sink = *sink; - } - else { - ctx->sink.on_progress = NULL; - ctx->sink.on_log = NULL; - ctx->sink.user_data = NULL; - } - // update telemetry copy; safe because sink is read-only - // by consumer after set - ctx->telemetry.sink = ctx->sink; -} - -/// Reports progress for an isolated `GPercentContext` instance. -/// -/// This re-entrant variant of `G_percent` is intended for concurrent or -/// context-specific work. It validates that `ctx` is initialized, clamps -/// `current_element` to the valid `0...total` range, and enqueues a progress -/// event only when the computed percentage reaches the next configured -/// threshold for the context. -/// -/// Callers typically create the context with `G_percent_context_create()`, call -/// this function as work advances, and later release resources with -/// `G_percent_context_destroy()`. -/// -/// Example: -/// ```c -/// size_t n_rows = window.rows; // total number of rows -/// size_t step = 10; // output step, every 10% -/// GProgressContext *ctx = G_progress_context_create(n_rows, step); -/// for (row = 0; row < window.rows; row++) { -/// // costly calculation ... -/// -/// // note: not counting from zero, as for loop never reaches n_rows -/// // and we want to reach 100% -/// size_t completed_row = row + 1; -/// -/// G_progress_update(ctx, completed_row); -/// } -/// G_progress_context_destroy(ctx); -/// ``` -/// -/// \param ctx The progress-reporting context created by -/// `G_percent_context_create()`. -/// \param completed: The current completed element index or count. -void G_progress_update(GProgressContext *ctx, size_t completed) -{ - if (!ctx) - return; - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) - return; - - telemetry_t *t = &ctx->telemetry; - if (t->total == 0) - return; - - size_t total = t->total; - if (completed > total) - completed = total; - - atomic_store_explicit(&t->completed, completed, memory_order_release); - ctx->report_progress(t, completed); -} - -void G_progress_increment(GProgressContext *ctx, size_t step) -{ - if (!ctx || step == 0) - return; - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) - return; - - telemetry_progress(&ctx->telemetry, step); -} - -void G_progress_tick(GProgressContext *ctx) -{ - G_progress_increment(ctx, 1); -} - -void G_progress_log(GProgressContext *ctx, const char *message) -{ - if (!ctx || !message) - return; - if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) - return; - telemetry_log(&ctx->telemetry, message); -} - -/// Creates and initializes a progress reporting context. -/// -/// The created context configures its reporting mode based on `step`. When -/// `step` is `0`, progress updates are emitted using a time-based interval -/// controlled by `interval_ms`. Otherwise, progress updates are emitted at -/// percentage increments defined by `step`. -/// -/// \param total_num_elements Total number of elements expected for the -/// operation being tracked. -/// \param step Percentage increment for reporting progress. A value of `0` -/// selects time-based reporting instead. -/// \param interval_ms Time interval in milliseconds between progress updates -/// when `step` is `0`. -/// \return A newly allocated and initialized `GProgressContext`, or `NULL` -/// if output is currently silenced. -static GProgressContext *context_create(size_t total_num_elements, size_t step, - long interval_ms) -{ - if (output_is_silenced()) - return NULL; - - GProgressContext *ctx = G_calloc(1, sizeof(*ctx)); - - atomic_init(&ctx->initialized, true); - - assert(step <= 100); - - if (step == 0) { - assert(interval_ms > 0); - telemetry_init_time(&ctx->telemetry, total_num_elements, interval_ms); - ctx->report_progress = context_progress_time; - } - else { - telemetry_init_percent(&ctx->telemetry, total_num_elements, step); - ctx->report_progress = context_progress_percent; - } - - ctx->sink.on_progress = NULL; - ctx->sink.on_log = NULL; - ctx->sink.user_data = NULL; - - // propagate context sink to telemetry by default - ctx->telemetry.sink = ctx->sink; - - atomic_init(&ctx->consumer_started, false); - - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &ctx->consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_create(&ctx->consumer_thread, NULL, telemetry_consumer, - &ctx->telemetry); - } - - return ctx; -} - -static void context_progress_percent(telemetry_t *t, size_t completed) -{ - size_t total = t->total; - - if (completed == total) { - telemetry_enqueue_final_progress(t); - return; - } - - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = - atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + t->percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &t->next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(t, &ev); - return; - } - } -} - -static void context_progress_time(telemetry_t *t, size_t completed) -{ - if (completed == t->total) { - telemetry_enqueue_final_progress(t); - return; - } - - long now = now_ns(); - long last = - atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); - - if (now - last < t->interval_ns) { - return; - } - if (!atomic_compare_exchange_strong_explicit(&t->last_progress_ns, &last, - now, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = t->total; - enqueue_event(t, &ev); -} - -/// Consumes queued telemetry events and emits log or progress output until -/// shutdown is requested and the event buffer has been drained. -/// -/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and -/// formatting settings should be consumed. -/// \return `NULL` after the consumer loop exits and any global consumer state -/// has been reset. -static void *telemetry_consumer(void *arg) -{ - telemetry_t *t = arg; - - while (true) { - if (atomic_load_explicit(&t->stop, memory_order_acquire) && - !telemetry_has_pending_events(t)) { - break; - } - - event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; - - if (!atomic_load_explicit(&ev->ready, memory_order_acquire)) { - sched_yield(); - continue; - } - - // handle event - if (ev->type == EV_LOG) { - if (t->sink.on_log) { - t->sink.on_log(ev->message, t->sink.user_data); - } - else { - sink_log_default(ev->message, NULL); - } - } - else if (ev->type == EV_PROGRESS) { - double pct = (ev->total > 0) - ? (double)ev->completed * 100.0 / (double)ev->total - : 0.0; - bool is_terminal = (ev->total > 0 && ev->completed >= ev->total); - - if (t->sink.on_progress) { - GProgressEvent pe = { - .completed = ev->completed, - .total = ev->total, - .percent = pct, - .is_terminal = is_terminal, - }; - t->sink.on_progress(&pe, t->sink.user_data); - } - else { - // Ensure defaults exist (defensive, should already be set at - // init) - telemetry_install_default_sink(t); - if (t->sink.on_progress) { - GProgressEvent pe = { - .completed = ev->completed, - .total = ev->total, - .percent = pct, - .is_terminal = is_terminal, - }; - t->sink.on_progress(&pe, t->sink.user_data); - } - } - } - - // mark slot free - atomic_store_explicit(&ev->ready, false, memory_order_release); - t->read_index++; - } - - if (t == &g_percent_telemetry) { - atomic_store_explicit(&g_percent_consumer_started, false, - memory_order_release); - atomic_store_explicit(&g_percent_initialized, false, - memory_order_release); - // keep g_percent_sink as-is; no change needed on shutdown - } - - return NULL; -} - -static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) -{ - atomic_init(&t->write_index, 0); - t->read_index = 0; - - for (size_t i = 0; i < LOG_CAPACITY; ++i) { - atomic_init(&t->buffer[i].ready, false); - } - - atomic_init(&t->completed, 0); - t->total = total; - telemetry_set_info_format(t); - - atomic_init(&t->last_progress_ns, 0); - t->interval_ns = interval_ms * 1000000L; - - t->percent_step = 0; // 0 => disabled, use time-based if interval_ns > 0 - atomic_init(&t->next_percent_threshold, 0); - - atomic_init(&t->stop, false); - - // default: no custom sink; callbacks NULL imply fallback to info_format - t->sink.on_progress = NULL; - t->sink.on_log = NULL; - t->sink.user_data = NULL; - telemetry_install_default_sink(t); -} - -/// Initializes telemetry state for percentage-based progress reporting. -/// -/// Resets the telemetry ring buffer and counters, disables time-based -/// throttling, and configures the next progress event to be emitted when the -/// completed work reaches the first `percent_step` threshold. -/// -/// \param t The telemetry instance to reset and configure. -/// \param total The total number of work units expected for the tracked -/// operation. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. A value of `0` disables percentage-based -/// thresholds. -static void telemetry_init_percent(telemetry_t *t, size_t total, - size_t percent_step) -{ - atomic_init(&t->write_index, 0); - t->read_index = 0; - for (size_t i = 0; i < LOG_CAPACITY; ++i) { - atomic_init(&t->buffer[i].ready, false); - } - atomic_init(&t->completed, 0); - t->total = total; - telemetry_set_info_format(t); - - // disable time-based gating - atomic_init(&t->last_progress_ns, 0); - t->interval_ns = 0; - - // enable percentage-based gating - t->percent_step = percent_step; - size_t first = percent_step > 0 ? percent_step : 0; - atomic_init(&t->next_percent_threshold, first); - - atomic_init(&t->stop, false); - - // default: no custom sink; callbacks NULL imply fallback to info_format - t->sink.on_progress = NULL; - t->sink.on_log = NULL; - t->sink.user_data = NULL; - telemetry_install_default_sink(t); -} - -/// Queues a telemetry event into the ring buffer for later consumption. -/// -/// Waits until the destination slot becomes available, copies the event payload -/// into that slot, and then marks the slot as ready using release semantics so -/// readers can safely observe the published event. -/// -/// \param t The telemetry instance that owns the event buffer. -/// \param src The event payload to enqueue. -static void enqueue_event(telemetry_t *t, event_t *src) -{ - size_t idx = - atomic_fetch_add_explicit(&t->write_index, 1, memory_order_relaxed); - - event_t *dst = &t->buffer[idx % LOG_CAPACITY]; - - // wait until slot is free (bounded spin) - while (atomic_load_explicit(&dst->ready, memory_order_acquire)) { - sched_yield(); - } - - // copy payload - *dst = *src; - - // publish - atomic_store_explicit(&dst->ready, true, memory_order_release); -} - -/// Queues a terminal `100%` progress event for a telemetry stream. -/// -/// This helper records the stream as fully completed, disables further -/// percentage-threshold reporting, and enqueues one last progress event with -/// `completed == total` so the consumer can emit the final `100%` update. -/// -/// \param t The telemetry instance to finalize. -static void telemetry_enqueue_final_progress(telemetry_t *t) -{ - event_t ev = {0}; - - atomic_store_explicit(&t->completed, t->total, memory_order_release); - atomic_store_explicit(&t->next_percent_threshold, 101, - memory_order_release); - - ev.type = EV_PROGRESS; - ev.completed = t->total; - ev.total = t->total; - enqueue_event(t, &ev); -} - -static bool telemetry_has_pending_events(telemetry_t *t) -{ - if (t->read_index != - atomic_load_explicit(&t->write_index, memory_order_acquire)) { - return true; - } - - event_t *ev = &t->buffer[t->read_index % LOG_CAPACITY]; - return atomic_load_explicit(&ev->ready, memory_order_acquire); -} - -static void telemetry_log(telemetry_t *t, const char *msg) -{ - event_t ev = {0}; - ev.type = EV_LOG; - snprintf(ev.message, LOG_MSG_SIZE, "%s", msg); - - enqueue_event(t, &ev); -} - -/// Captures the current GRASS info output format for subsequent telemetry. -/// -/// Reads the process-wide info formatting mode and stores it on the telemetry -/// instance so later progress and log events can format output consistently. -/// -/// \param t The telemetry state that caches the active info format. -static void telemetry_set_info_format(telemetry_t *t) -{ - t->info_format = G_info_format(); -} - -/// Records completed work and enqueues a progress event when the next -/// reportable threshold is reached. -/// -/// The function atomically increments the telemetry's completed counter by -/// `step`, then decides whether to emit a progress event using one of two -/// modes: percent-based reporting when `percent_step` and `total` are -/// configured, or time-based throttling when they are not. Atomic -/// compare-and-swap operations ensure that only one caller emits an event for a -/// given threshold or interval. -/// -/// \param t The telemetry state to update and publish through. -/// \param step The number of newly completed units of work to add. -static void telemetry_progress(telemetry_t *t, size_t step) -{ - size_t new_completed = - atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + - step; - - if (t->percent_step > 0 && t->total > 0) { - if (new_completed >= t->total) { - telemetry_enqueue_final_progress(t); - return; - } - - size_t current_pct = (size_t)((new_completed * 100) / t->total); - size_t expected = atomic_load_explicit(&t->next_percent_threshold, - memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + t->percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; // sentinel beyond 100 to stop further emits - if (atomic_compare_exchange_strong_explicit( - &t->next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - // we won the right to emit at this threshold - break; - } - // CAS failed, expected now contains the latest value; loop to - // re-check - } - // If we didn't advance, nothing to emit - if (current_pct < expected || expected > 100) { - return; - } - } - else { - long now = now_ns(); - long last = - atomic_load_explicit(&t->last_progress_ns, memory_order_relaxed); - if (now - last < t->interval_ns) { - return; - } - if (!atomic_compare_exchange_strong_explicit( - &t->last_progress_ns, &last, now, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - } - - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = new_completed; - ev.total = t->total; - - enqueue_event(t, &ev); -} - -static void telemetry_install_default_sink(telemetry_t *t) -{ - // Only set defaults if no custom sink is present - if (t->sink.on_progress || t->sink.on_log) - return; - - switch (t->info_format) { - case G_INFO_FORMAT_STANDARD: - t->sink.on_progress = sink_progress_standard; - break; - case G_INFO_FORMAT_GUI: - t->sink.on_progress = sink_progress_gui; - break; - case G_INFO_FORMAT_PLAIN: - t->sink.on_progress = sink_progress_plain; - break; - default: - t->sink.on_progress = NULL; // silent/no output - break; - } - t->sink.on_log = sink_log_default; - t->sink.user_data = NULL; -} - -/// Initializes shared percent-based telemetry and starts the detached consumer -/// thread once. -/// -/// This function performs one-time global setup for percent progress reporting. -/// Repeated calls return immediately after the initialization state has been -/// set. If output is disabled or the consumer thread cannot be created, no -/// further progress consumer setup is performed. -/// -/// \param total_num_elements The total number of elements used to compute -/// progress percentages. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. -static void start_global_percent(size_t total_num_elements, size_t percent_step) -{ - bool expected_init = false; - if (!atomic_compare_exchange_strong_explicit( - &g_percent_initialized, &expected_init, true, memory_order_acq_rel, - memory_order_relaxed)) { - return; - } - - telemetry_init_percent(&g_percent_telemetry, - ((total_num_elements > 0) ? total_num_elements : 0), - ((percent_step > 0) ? percent_step : 0)); - - // attach current global sink (may be empty for default behavior) - if (g_percent_sink.on_progress || g_percent_sink.on_log) { - g_percent_telemetry.sink = g_percent_sink; - } // else keep defaults installed by telemetry_init_percent - - bool expected_started = false; - if (atomic_compare_exchange_strong_explicit( - &g_percent_consumer_started, &expected_started, true, - memory_order_acq_rel, memory_order_relaxed)) { - pthread_t consumer_thread; - pthread_attr_t attr; - - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - if (pthread_create(&consumer_thread, &attr, telemetry_consumer, - &g_percent_telemetry) != 0) { - atomic_store_explicit(&g_percent_consumer_started, false, - memory_order_release); - atomic_store_explicit(&g_percent_initialized, false, - memory_order_release); - } - pthread_attr_destroy(&attr); - } -} - -/// Sets or clears the global sink used by `G_percent` progress reporting. -/// -/// Copies `sink` into the shared global progress configuration used by the -/// legacy `G_percent` API. When `sink` is non-`NULL`, its callbacks and -/// `user_data` are used for subsequent progress and log events. Passing `NULL` -/// clears the custom sink and restores the default output behavior derived from -/// the current runtime info format. -/// -/// If global progress telemetry has already been initialized, the active -/// telemetry sink is updated immediately so later events follow the new -/// configuration. -/// -/// \param sink The sink configuration to install globally, or `NULL` to remove -/// the custom sink and fall back to the default renderer. -static void set_global_sink(const GProgressSink *sink) -{ - if (sink) { - g_percent_sink = *sink; - } - else { - g_percent_sink.on_progress = NULL; - g_percent_sink.on_log = NULL; - g_percent_sink.user_data = NULL; - } - - // apply to global telemetry if initialized - if (atomic_load_explicit(&g_percent_initialized, memory_order_acquire)) { - if (g_percent_sink.on_progress || g_percent_sink.on_log) { - g_percent_telemetry.sink = g_percent_sink; - } - else { - // reinstall defaults based on current info_format - g_percent_telemetry.sink.on_progress = NULL; - g_percent_telemetry.sink.on_log = NULL; - g_percent_telemetry.sink.user_data = NULL; - telemetry_install_default_sink(&g_percent_telemetry); - } - } -} - -static bool output_is_silenced(void) -{ - return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); -} - -/// Returns the current UTC time in nanoseconds. -static long now_ns(void) -{ -#if defined(TIME_UTC) - struct timespec ts; - timespec_get(&ts, TIME_UTC); - return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; -#elif defined(CLOCK_REALTIME) - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - return (long)ts.tv_sec * 1000000000L + ts.tv_nsec; -#elif defined(HAVE_GETTIMEOFDAY) - struct timeval tv; - gettimeofday(&tv, NULL); - return (long)tv.tv_sec * 1000000000L + (long)tv.tv_usec * 1000L; -#else - return (long)time(NULL) * 1000000000L; -#endif -} - -// Legacy compatibility: adapter for void (*fn)(int) -static void legacy_percent_adapter(const GProgressEvent *e, void *ud) -{ - int (*fn)(int) = g_legacy_percent_routine; - (void)ud; - if (fn) { - int pct = (int)(e->percent); - (void)fn(pct); - } -} - -// Internal default sinks for different G_info_format modes -static void sink_progress_standard(const GProgressEvent *e, void *ud) -{ - (void)ud; - if (e->total == 0) { - fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", e->completed); - return; - } - int pct = (int)(e->percent); - fprintf(stderr, "%4d%%\b\b\b\b\b", pct); - if (pct == 100) - fprintf(stderr, "\n"); -} - -static void sink_progress_plain(const GProgressEvent *e, void *ud) -{ - (void)ud; - if (e->total == 0) { - fprintf(stderr, "%zu..", e->completed); - return; - } - int pct = (int)(e->percent); - fprintf(stderr, "%d%s", pct, (pct == 100 ? "\n" : "..")); -} - -static void sink_progress_gui(const GProgressEvent *e, void *ud) -{ - (void)ud; - if (e->total == 0) { - fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", e->completed); - fflush(stderr); - return; - } - int pct = (int)(e->percent); - - int comp = (int)e->completed; - int tot = (int)e->total; - - fprintf(stderr, "GRASS_INFO_PERCENT: %d (%d/%d)\n", pct, comp, tot); - fflush(stderr); -} - -static void sink_log_default(const char *message, void *ud) -{ - (void)ud; - // default logging to stdout - printf("[LOG] %s\n", message); -} - -// Legacy API - -/// Reports global progress when completion crosses the next percentage step. -/// -/// This routine prints a percentage complete message to stderr. The -/// percentage complete is `(current_element/total_num_elements)\*100`, and -/// these are printed only for each __s__ percentage. This is perhaps best -/// explained by example: -/// -/// ```c -/// #include -/// #include -/// -/// int nrows = 1352; // 1352 is not a special value - example only -/// -/// G_message(_("Percent complete...")); -/// for (int row = 0; row < nrows; row++) -/// { -/// G_percent(row, nrows, 10); -/// do_calculation(row); -/// } -/// G_percent(1, 1, 1); -/// ``` -/// -/// This example code will print completion messages at 10% increments; -/// i.e., 0%, 10%, 20%, 30%, etc., up to 100%. Each message does not appear -/// on a new line, but rather erases the previous message. -/// -/// This function initializes the shared global telemetry stream on first use, -/// clamps `current_element` into the valid `0...total_num_elements` range, and -/// enqueues a progress update only when the computed percentage reaches the -/// next configured threshold. When progress reaches the total, a terminal -/// `100%` event is always queued and the background consumer is asked to stop -/// after pending events have been flushed. -/// -/// \param current_element The current completed element index or count. -/// \param total_num_elements The total number of elements to process. Values -/// less than or equal to `0` disable reporting. -/// \param percent_step The minimum percentage increment required before a new -/// progress event is emitted. -void G_percent(long current_element, long total_num_elements, int percent_step) -{ - if (total_num_elements <= 0 || output_is_silenced()) - return; - - start_global_percent((size_t)total_num_elements, (size_t)percent_step); - - // If someone initialized with different totals/steps, we keep the first - // ones for simplicity. - - size_t total = (size_t)total_num_elements; - size_t completed = (current_element < 0) ? 0 : (size_t)current_element; - if (completed > total) - completed = total; - - if (g_percent_telemetry.percent_step == 0) - return; // not configured - - atomic_store_explicit(&g_percent_telemetry.completed, completed, - memory_order_release); - - if (completed == total) { - telemetry_enqueue_final_progress(&g_percent_telemetry); - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - return; - } - - size_t current_pct = (size_t)((completed * 100) / total); - size_t expected = atomic_load_explicit( - &g_percent_telemetry.next_percent_threshold, memory_order_relaxed); - while (current_pct >= expected && expected <= 100) { - size_t next = expected + g_percent_telemetry.percent_step; - if (expected < 100 && next > 100) - next = 100; - else if (next > 100) - next = 101; - if (atomic_compare_exchange_strong_explicit( - &g_percent_telemetry.next_percent_threshold, &expected, next, - memory_order_acq_rel, memory_order_relaxed)) { - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = completed; - ev.total = total; - enqueue_event(&g_percent_telemetry, &ev); - if (completed == total) { - atomic_store_explicit(&g_percent_telemetry.stop, true, - memory_order_release); - } - return; - } - // CAS failed; expected updated, loop continues - } -} - -/// Reset G_percent() to 0%; do not add newline. -/// -/// \note Transitional no-op: retained for compatibility with legacy callers. -/// -/// \deprecated For new or updated code G_percent_reset() is not needed. -void G_percent_reset(void) -{ - // No global state to reset in the concurrent API. - // Kept to avoid breaking legacy code paths that call G_percent_reset(). -} - -/// Print progress info messages -/// -/// Use G_progress_update(), or G_percent() for legacy code, when number of -/// elements is defined. -/// -/// This routine prints a progress info message to stderr. The value -/// `n` is printed only for each `s`. This is perhaps best -/// explained by example: -/// -/// ```c -/// #include -/// -/// int line = 0; - -/// G_message(_("Reading features...")); -/// while(TRUE) -/// { -/// if (Vect_read_next_line(Map, Points, Cats) < 0) -/// break; -/// line++; -/// G_progress(line, 1e3); -/// } -/// G_progress(1, 1); -/// ``` -/// -/// This example code will print progress in messages at 1000 increments; -/// i.e., 1000, 2000, 3000, 4000, etc., up to number of features for -/// given vector map. Each message does not appear on a new line, but -/// rather erases the previous message. -/// -/// \param n current element -/// \param s increment size -void G_progress(long n, int s) -{ - // Mirror legacy behavior: emit on multiples of s, and handle first tick - // formatting. We route through the global telemetry so it benefits from the - // consumer thread. - - if (s <= 0 || output_is_silenced()) - return; - - // Initialize global telemetry if needed with percent_step=0 to disable - // percent thresholds - start_global_percent(0, 0); - - // Use time-based gating if an interval is configured; otherwise, we emit - // only on multiples of s. Here, we implement the modulo gating explicitly. - - if (n == s && n == 1) { - // For default sink, legacy prints a leading CR/Newline depending on - // format. We simulate this by enqueueing a LOG event when using default - // sink; custom sinks can ignore. - if (g_percent_telemetry.sink.on_progress == NULL) { - switch (g_percent_telemetry.info_format) { - case G_INFO_FORMAT_PLAIN: - telemetry_log(&g_percent_telemetry, "\n"); - break; - case G_INFO_FORMAT_GUI: - // No-op; GUI variant prints on progress events - break; - default: - // STANDARD and others: carriage return - telemetry_log(&g_percent_telemetry, "\n"); - break; - } - } - return; - } - - if (n % s != 0) - return; - - // For counter mode, we do not know total; publish completed=n, total=0 - event_t ev = {0}; - ev.type = EV_PROGRESS; - ev.completed = (n < 0 ? 0 : (size_t)n); - ev.total = 0; // unknown total; consumer/sink can render raw counts - enqueue_event(&g_percent_telemetry, &ev); -} - -// Compatibility layer for legacy percent routine API -/// Establishes percent_routine as the routine that will handle the printing of -/// percentage progress messages. -/// -/// \param percent_routine routine will be called like this: percent_routine(x) - -void G_set_percent_routine(int (*fn)(int)) -{ - // The historical signature in gis.h declares int (*)(int), but actual - // implementers often used void(*)(int). We accept int-returning and ignore - // the return value. - if (!fn) { - // Reset to default behavior - g_legacy_percent_routine = NULL; - set_global_sink(NULL); - return; - } - // Route legacy callbacks through a dedicated function-pointer slot. - GProgressSink s = {0}; - s.on_progress = legacy_percent_adapter; - s.user_data = NULL; - g_legacy_percent_routine = fn; - set_global_sink(&s); -} - -void G_unset_percent_routine(void) -{ - // Reset to default (env-driven G_info_format output) - set_global_sink(NULL); -} - -#endif // defined(G_USE_PROGRESS_NG) - -#endif // 0 From a50652fe7796b594adbd10d7fc2aff8cafc6748c Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 12:06:40 +0200 Subject: [PATCH 20/23] fixes --- include/grass/defs/gis.h | 2 -- lib/gis/percent.c | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index cefb4e34cc2..c59ca7f899d 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -668,8 +668,6 @@ void G_percent_reset(void); void G_progress(long, int); void G_set_percent_routine(int (*)(int)); void G_unset_percent_routine(void); - -/* progress.c */ #if defined(G_USE_PROGRESS_NG) GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 46c4d7e110a..7e068a26b97 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -1277,7 +1277,7 @@ static void G__progress_ng(long n, int s) break; default: // STANDARD and others: carriage return - telemetry_log(&g_percent_telemetry, "\n"); + telemetry_log(&g_percent_telemetry, "\r"); break; } } From 6088f78c87225047386d7bc97fe6f0f9bbdcc456 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 18:13:28 +0200 Subject: [PATCH 21/23] fix docs, C-type --- lib/gis/percent.c | 460 +++++++++++++++++++++++++--------------------- 1 file changed, 248 insertions(+), 212 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 7e068a26b97..fa7a52d94e1 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -2,7 +2,7 @@ \file lib/gis/percent.c \brief GIS Library - Progress reporting and telemetry support for - GRASS operations + GRASS operations This file implements the `G_progress_*` API used to report incremental work completion for long-running operations. It supports both percentage-based @@ -294,11 +294,13 @@ typedef struct { atomic_bool ready; } event_t; -/// Internal telemetry state shared by the progress producer and consumer. -/// -/// `telemetry_t` owns the ring buffer of queued log/progress events, the -/// counters and thresholds used for time- or percent-based emission, and the -/// sink configuration that determines how flushed events are rendered. +/*! + \brief Internal telemetry state shared by the progress producer and consumer. + + `telemetry_t` owns the ring buffer of queued log/progress events, the + counters and thresholds used for time- or percent-based emission, and the + sink configuration that determines how flushed events are rendered. +*/ typedef struct { event_t buffer[LOG_CAPACITY]; atomic_size_t write_index; @@ -354,24 +356,26 @@ static void sink_progress_plain(const GProgressEvent *, void *); static void sink_progress_gui(const GProgressEvent *, void *); static void sink_log_default(const char *, void *); -/// Creates an isolated progress-reporting context for concurrent work. -/// -/// The returned context tracks progress for `total_num_elements` items and -/// emits progress updates whenever completion advances by at least -/// `percent_step` percentage points. `total_num_elements` must match the -/// actual number of work units that will be reported through -/// `G_progress_update()`. In particular, callers should pass a completed-work -/// count, not a raw loop index or a larger container size, otherwise the -/// terminal `100%` update may never be reached. If output is enabled by the -/// current runtime configuration, this function also starts the background -/// consumer thread used to flush queued telemetry events. -/// -/// \param total_num_elements Total number of elements to process. -/// \param step Minimum percentage increment that triggers a -/// progress event. -/// \return A newly allocated `GPercentContext`, or `NULL` if output -/// is silenced by environment variable `GRASS_MESSAGE_FORMAT` or -/// verbosity level is below `1`. +/*! + \brief Creates an isolated progress-reporting context for concurrent work. + + The returned context tracks progress for `total_num_elements` items and + emits progress updates whenever completion advances by at least + `percent_step` percentage points. `total_num_elements` must match the + actual number of work units that will be reported through + `G_progress_update()`. In particular, callers should pass a completed-work + count, not a raw loop index or a larger container size, otherwise the + terminal `100%` update may never be reached. If output is enabled by the + current runtime configuration, this function also starts the background + consumer thread used to flush queued telemetry events. + + \param total_num_elements Total number of elements to process. + \param step Minimum percentage increment that triggers a + progress event. + \return A newly allocated `GPercentContext`, or `NULL` if output + is silenced by environment variable `GRASS_MESSAGE_FORMAT` or + verbosity level is below `1`. + */ GProgressContext *G_progress_context_create(size_t total_num_elements, size_t step) { @@ -379,38 +383,43 @@ GProgressContext *G_progress_context_create(size_t total_num_elements, (step == 0 ? TIME_RATE_LIMIT_MS : 0)); } -/// Creates an isolated progress-reporting context with time-based updates. -/// -/// Unlike `G_progress_context_create()`, which emits progress events when -/// completion crosses percentage thresholds, this variant rate-limits progress -/// emission by elapsed time. The returned context tracks -/// `total_num_elements` work units and reports updates no more frequently than -/// once every `interval_ms` milliseconds while work is in progress. -/// -/// Callers should report monotonically increasing completed-work counts through -/// `G_progress_update()` and destroy the context with -/// `G_progress_context_destroy()` when processing finishes. -/// -/// \param total_num_elements Total number of elements to process. -/// \param interval_ms Minimum time interval, in milliseconds, between emitted -/// progress updates. -/// \return A newly allocated `GProgressContext`, or `NULL` if output is -/// silenced by the current runtime configuration. +/*! + \brief Creates an isolated progress-reporting context with time-based + updates. + + Unlike `G_progress_context_create()`, which emits progress events when + completion crosses percentage thresholds, this variant rate-limits progress + emission by elapsed time. The returned context tracks + `total_num_elements` work units and reports updates no more frequently than + once every `interval_ms` milliseconds while work is in progress. + + Callers should report monotonically increasing completed-work counts through + `G_progress_update()` and destroy the context with + `G_progress_context_destroy()` when processing finishes. + + \param total_num_elements Total number of elements to process. + \param interval_ms Minimum time interval, in milliseconds, between emitted + progress updates. + \return A newly allocated `GProgressContext`, or `NULL` if output is + silenced by the current runtime configuration. +*/ GProgressContext *G_progress_context_create_time(size_t total_num_elements, long interval_ms) { return context_create(total_num_elements, 0, interval_ms); } -/// Destroys a `GPercentContext` and releases any resources it owns. -/// -/// This function stops the context's background telemetry consumer, waits for -/// the consumer thread to finish when it was started, marks the context as no -/// longer initialized, and frees the context memory. Passing `NULL` is safe and -/// has no effect. -/// -/// \param ctx The progress-reporting context previously created by -/// `G_percent_context_create()`, or `NULL`. +/*! + \brief Destroys a `GPercentContext` and releases any resources it owns. + + This function stops the context's background telemetry consumer, waits for + the consumer thread to finish when it was started, marks the context as no + longer initialized, and frees the context memory. Passing `NULL` is safe and + has no effect. + + \param ctx The progress-reporting context previously created by + `G_percent_context_create()`, or `NULL`. +*/ void G_progress_context_destroy(GProgressContext *ctx) { if (!ctx) { @@ -440,28 +449,30 @@ void G_progress_context_destroy(GProgressContext *ctx) G_free(ctx); } -/// Sets or clears the output sink used by a progress context. -/// -/// Installs a per-context `GProgressSink` override for progress and log events -/// emitted by `ctx`. When `sink` is non-`NULL`, its callbacks and `user_data` -/// are copied into the context and used by the telemetry consumer. Passing -/// `NULL` clears any custom sink so the context falls back to its default -/// output behavior. -/// -/// \param ctx The progress context to update. If `NULL`, the function has -/// no effect. -/// \param sink The sink configuration to copy into the context, or `NULL` -/// to remove the custom sink. -/// -/// Example: -/// ```c -/// GProgressSink sink = { -/// .on_progress = my_progress_handler, -/// .on_log = my_log_handler, -/// .user_data = my_context, -/// }; -/// G_progress_context_set_sink(progress_ctx, &sink); -/// ``` +/*! + \brief Sets or clears the output sink used by a progress context. + + Installs a per-context `GProgressSink` override for progress and log events + emitted by `ctx`. When `sink` is non-`NULL`, its callbacks and `user_data` + are copied into the context and used by the telemetry consumer. Passing + `NULL` clears any custom sink so the context falls back to its default + output behavior. + + Example: + ```c + GProgressSink sink = { + .on_progress = my_progress_handler, + .on_log = my_log_handler, + .user_data = my_context, + }; + G_progress_context_set_sink(progress_ctx, &sink); + ``` + \param ctx The progress context to update. If `NULL`, the function has + no effect. + \param sink The sink configuration to copy into the context, or `NULL` + to remove the custom sink. + +*/ void G_progress_context_set_sink(GProgressContext *ctx, const GProgressSink *sink) { @@ -480,38 +491,40 @@ void G_progress_context_set_sink(GProgressContext *ctx, ctx->telemetry.sink = ctx->sink; } -/// Reports progress for an isolated `GProgressContext` instance. -/// -/// This re-entrant variant of `G_percent` is intended for concurrent or -/// context-specific work. It validates that `ctx` is initialized, clamps -/// `current_element` to the valid `0...total` range, and enqueues a progress -/// event only when the computed percentage reaches the next configured -/// threshold for the context. -/// -/// Callers typically create the context with `G_progress_context_create()`, -/// call this function as work advances, and later release resources with -/// `G_progress_context_destroy()`. -/// -/// Example: -/// ```c -/// size_t n_rows = window.rows; // total number of rows -/// size_t step = 10; // output step, every 10% -/// GProgressContext *ctx = G_progress_context_create(n_rows, step); -/// for (row = 0; row < window.rows; row++) { -/// // costly calculation ... -/// -/// // note: not counting from zero, as for loop never reaches n_rows -/// // and we want to reach 100% -/// size_t completed_row = row + 1; -/// -/// G_progress_update(ctx, completed_row); -/// } -/// G_progress_context_destroy(ctx); -/// ``` -/// -/// \param ctx The progress-reporting context created by -/// `G_percent_context_create()`. -/// \param completed: The current completed element index or count. +/*! + \brief Reports progress for an isolated `GProgressContext` instance. + + This re-entrant variant of `G_percent` is intended for concurrent or + context-specific work. It validates that `ctx` is initialized, clamps + `current_element` to the valid `0...total` range, and enqueues a progress + event only when the computed percentage reaches the next configured + threshold for the context. + + Callers typically create the context with `G_progress_context_create()`, + call this function as work advances, and later release resources with + `G_progress_context_destroy()`. + + Example: + ```c + size_t n_rows = window.rows; // total number of rows + size_t step = 10; // output step, every 10% + GProgressContext *ctx = G_progress_context_create(n_rows, step); + for (row = 0; row < window.rows; row++) { + // costly calculation ... + + // note: not counting from zero, as for loop never reaches n_rows + // and we want to reach 100% + size_t completed_row = row + 1; + + G_progress_update(ctx, completed_row); + } + G_progress_context_destroy(ctx); + ``` + + \param ctx The progress-reporting context created by + `G_percent_context_create()`. + \param completed The current completed element index or count. +*/ void G_progress_update(GProgressContext *ctx, size_t completed) { if (!ctx) @@ -555,21 +568,23 @@ void G_progress_log(GProgressContext *ctx, const char *message) telemetry_log(&ctx->telemetry, message); } -/// Creates and initializes a progress reporting context. -/// -/// The created context configures its reporting mode based on `step`. When -/// `step` is `0`, progress updates are emitted using a time-based interval -/// controlled by `interval_ms`. Otherwise, progress updates are emitted at -/// percentage increments defined by `step`. -/// -/// \param total_num_elements Total number of elements expected for the -/// operation being tracked. -/// \param step Percentage increment for reporting progress. A value of `0` -/// selects time-based reporting instead. -/// \param interval_ms Time interval in milliseconds between progress updates -/// when `step` is `0`. -/// \return A newly allocated and initialized `GProgressContext`, or `NULL` -/// if output is currently silenced. +/*! + \brief Creates and initializes a progress reporting context. + + The created context configures its reporting mode based on `step`. When + `step` is `0`, progress updates are emitted using a time-based interval + controlled by `interval_ms`. Otherwise, progress updates are emitted at + percentage increments defined by `step`. + + \param total_num_elements Total number of elements expected for the + operation being tracked. + \param step Percentage increment for reporting progress. A value of `0` + selects time-based reporting instead. + \param interval_ms Time interval in milliseconds between progress updates + when `step` is `0`. + \return A newly allocated and initialized `GProgressContext`, or `NULL` + if output is currently silenced. +*/ static GProgressContext *context_create(size_t total_num_elements, size_t step, long interval_ms) { @@ -670,13 +685,15 @@ static void context_progress_time(telemetry_t *t, size_t completed) enqueue_event(t, &ev); } -/// Consumes queued telemetry events and emits log or progress output until -/// shutdown is requested and the event buffer has been drained. -/// -/// \param arg Pointer to the `telemetry_t` instance whose ring buffer and -/// formatting settings should be consumed. -/// \return `NULL` after the consumer loop exits and any global consumer state -/// has been reset. +/*! + \brief Consumes queued telemetry events and emits log or progress output + until shutdown is requested and the event buffer has been drained. + + \param arg Pointer to the `telemetry_t` instance whose ring buffer and + formatting settings should be consumed. + \return `NULL` after the consumer loop exits and any global consumer state + has been reset. +*/ static void *telemetry_consumer(void *arg) { telemetry_t *t = arg; @@ -778,18 +795,20 @@ static void telemetry_init_time(telemetry_t *t, size_t total, long interval_ms) telemetry_install_default_sink(t); } -/// Initializes telemetry state for percentage-based progress reporting. -/// -/// Resets the telemetry ring buffer and counters, disables time-based -/// throttling, and configures the next progress event to be emitted when the -/// completed work reaches the first `percent_step` threshold. -/// -/// \param t The telemetry instance to reset and configure. -/// \param total The total number of work units expected for the tracked -/// operation. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. A value of `0` disables percentage-based -/// thresholds. +/*! + \brief Initializes telemetry state for percentage-based progress reporting. + + Resets the telemetry ring buffer and counters, disables time-based + throttling, and configures the next progress event to be emitted when the + completed work reaches the first `percent_step` threshold. + + \param t The telemetry instance to reset and configure. + \param total The total number of work units expected for the tracked + operation. + \param percent_step The percentage increment that controls when + progress updates are emitted. A value of `0` disables percentage-based + thresholds. +*/ static void telemetry_init_percent(telemetry_t *t, size_t total, size_t percent_step) { @@ -820,14 +839,16 @@ static void telemetry_init_percent(telemetry_t *t, size_t total, telemetry_install_default_sink(t); } -/// Queues a telemetry event into the ring buffer for later consumption. -/// -/// Waits until the destination slot becomes available, copies the event payload -/// into that slot, and then marks the slot as ready using release semantics so -/// readers can safely observe the published event. -/// -/// \param t The telemetry instance that owns the event buffer. -/// \param src The event payload to enqueue. +/*! + \brief Queues a telemetry event into the ring buffer for later consumption. + + Waits until the destination slot becomes available, copies the event payload + into that slot, and then marks the slot as ready using release semantics so + readers can safely observe the published event. + + \param t The telemetry instance that owns the event buffer. + \param src The event payload to enqueue. +*/ static void enqueue_event(telemetry_t *t, event_t *src) { size_t idx = @@ -847,13 +868,15 @@ static void enqueue_event(telemetry_t *t, event_t *src) atomic_store_explicit(&dst->ready, true, memory_order_release); } -/// Queues a terminal `100%` progress event for a telemetry stream. -/// -/// This helper records the stream as fully completed, disables further -/// percentage-threshold reporting, and enqueues one last progress event with -/// `completed == total` so the consumer can emit the final `100%` update. -/// -/// \param t The telemetry instance to finalize. +/*! + \brief Queues a terminal `100%` progress event for a telemetry stream. + + This helper records the stream as fully completed, disables further + percentage-threshold reporting, and enqueues one last progress event with + `completed == total` so the consumer can emit the final `100%` update. + + \param t The telemetry instance to finalize. +*/ static void telemetry_enqueue_final_progress(telemetry_t *t) { event_t ev = {0}; @@ -888,29 +911,34 @@ static void telemetry_log(telemetry_t *t, const char *msg) enqueue_event(t, &ev); } -/// Captures the current GRASS info output format for subsequent telemetry. -/// -/// Reads the process-wide info formatting mode and stores it on the telemetry -/// instance so later progress and log events can format output consistently. -/// -/// \param t The telemetry state that caches the active info format. +/*! + \brief Captures the current GRASS info output format for subsequent + telemetry. + + Reads the process-wide info formatting mode and stores it on the telemetry + instance so later progress and log events can format output consistently. + + \param t The telemetry state that caches the active info format. +*/ static void telemetry_set_info_format(telemetry_t *t) { t->info_format = G_info_format(); } -/// Records completed work and enqueues a progress event when the next -/// reportable threshold is reached. -/// -/// The function atomically increments the telemetry's completed counter by -/// `step`, then decides whether to emit a progress event using one of two -/// modes: percent-based reporting when `percent_step` and `total` are -/// configured, or time-based throttling when they are not. Atomic -/// compare-and-swap operations ensure that only one caller emits an event for a -/// given threshold or interval. -/// -/// \param t The telemetry state to update and publish through. -/// \param step The number of newly completed units of work to add. +/*! + \brief Records completed work and enqueues a progress event when the next + reportable threshold is reached. + + The function atomically increments the telemetry's completed counter by + `step`, then decides whether to emit a progress event using one of two + modes: percent-based reporting when `percent_step` and `total` are + configured, or time-based throttling when they are not. Atomic + compare-and-swap operations ensure that only one caller emits an event for a + given threshold or interval. + + \param t The telemetry state to update and publish through. + \param step The number of newly completed units of work to add. +*/ static void telemetry_progress(telemetry_t *t, size_t step) { size_t new_completed = @@ -992,18 +1020,20 @@ static void telemetry_install_default_sink(telemetry_t *t) t->sink.user_data = NULL; } -/// Initializes shared percent-based telemetry and starts the detached consumer -/// thread once. -/// -/// This function performs one-time global setup for percent progress reporting. -/// Repeated calls return immediately after the initialization state has been -/// set. If output is disabled or the consumer thread cannot be created, no -/// further progress consumer setup is performed. -/// -/// \param total_num_elements The total number of elements used to compute -/// progress percentages. -/// \param percent_step The percentage increment that controls when -/// progress updates are emitted. +/*! + \brief Initializes shared percent-based telemetry and starts the detached + consumer thread once. + + This function performs one-time global setup for percent progress reporting. + Repeated calls return immediately after the initialization state has been + set. If output is disabled or the consumer thread cannot be created, no + further progress consumer setup is performed. + + \param total_num_elements The total number of elements used to compute + progress percentages. + \param percent_step The percentage increment that controls when + progress updates are emitted. +*/ static void start_global_percent(size_t total_num_elements, size_t percent_step) { bool expected_init = false; @@ -1042,20 +1072,23 @@ static void start_global_percent(size_t total_num_elements, size_t percent_step) } } -/// Sets or clears the global sink used by `G_percent` progress reporting. -/// -/// Copies `sink` into the shared global progress configuration used by the -/// legacy `G_percent` API. When `sink` is non-`NULL`, its callbacks and -/// `user_data` are used for subsequent progress and log events. Passing `NULL` -/// clears the custom sink and restores the default output behavior derived from -/// the current runtime info format. -/// -/// If global progress telemetry has already been initialized, the active -/// telemetry sink is updated immediately so later events follow the new -/// configuration. -/// -/// \param sink The sink configuration to install globally, or `NULL` to remove -/// the custom sink and fall back to the default renderer. +/*! + \brief Sets or clears the global sink used by `G_percent` progress + reporting. + + Copies `sink` into the shared global progress configuration used by the + legacy `G_percent` API. When `sink` is non-`NULL`, its callbacks and + `user_data` are used for subsequent progress and log events. Passing `NULL` + clears the custom sink and restores the default output behavior derived from + the current runtime info format. + + If global progress telemetry has already been initialized, the active + telemetry sink is updated immediately so later events follow the new + configuration. + + \param sink The sink configuration to install globally, or `NULL` to remove + the custom sink and fall back to the default renderer. +*/ static void set_global_sink(const GProgressSink *sink) { if (sink) { @@ -1087,7 +1120,7 @@ static bool output_is_silenced(void) return (G_info_format() == G_INFO_FORMAT_SILENT || G_verbose() < 1); } -/// Returns the current UTC time in nanoseconds. +/*! \brief Returns the current UTC time in nanoseconds. */ static long now_ns(void) { #if defined(TIME_UTC) @@ -1169,20 +1202,23 @@ static void sink_log_default(const char *message, void *ud) // Legacy API -/// Reports global progress when completion crosses the next percentage step. -/// -/// This function initializes the shared global telemetry stream on first use, -/// clamps `current_element` into the valid `0...total_num_elements` range, and -/// enqueues a progress update only when the computed percentage reaches the -/// next configured threshold. When progress reaches the total, a terminal -/// `100%` event is always queued and the background consumer is asked to stop -/// after pending events have been flushed. -/// -/// \param current_element The current completed element index or count. -/// \param total_num_elements The total number of elements to process. Values -/// less than or equal to `0` disable reporting. -/// \param percent_step The minimum percentage increment required before a new -/// progress event is emitted. +/*! + \brief Reports global progress when completion crosses the next percentage + step. + + This function initializes the shared global telemetry stream on first use, + clamps `current_element` into the valid `0...total_num_elements` range, and + enqueues a progress update only when the computed percentage reaches the + next configured threshold. When progress reaches the total, a terminal + `100%` event is always queued and the background consumer is asked to stop + after pending events have been flushed. + + \param current_element The current completed element index or count. + \param total_num_elements The total number of elements to process. Values + less than or equal to `0` disable reporting. + \param percent_step The minimum percentage increment required before a new + progress event is emitted. +*/ static void G__percent_ng(long current_element, long total_num_elements, int percent_step) { From 43396746f5a68ed69de96740454d814fffde014e Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Wed, 1 Apr 2026 22:47:30 +0200 Subject: [PATCH 22/23] fix indefinite progress --- include/grass/defs/gis.h | 2 +- lib/gis/percent.c | 128 +++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 33 deletions(-) diff --git a/include/grass/defs/gis.h b/include/grass/defs/gis.h index c59ca7f899d..8593b097328 100644 --- a/include/grass/defs/gis.h +++ b/include/grass/defs/gis.h @@ -671,12 +671,12 @@ void G_unset_percent_routine(void); #if defined(G_USE_PROGRESS_NG) GProgressContext *G_progress_context_create(size_t, size_t); GProgressContext *G_progress_context_create_time(size_t, long); +GProgressContext *G_progress_context_create_increment(size_t); void G_progress_context_destroy(GProgressContext *); void G_progress_update(GProgressContext *, size_t); void G_progress_log(GProgressContext *, const char *); void G_progress_context_set_sink(GProgressContext *, const GProgressSink *); void G_progress_increment(GProgressContext *, size_t); -void G_progress_tick(GProgressContext *); #endif // G_USE_PROGRESS_NG /* popen.c */ diff --git a/lib/gis/percent.c b/lib/gis/percent.c index fa7a52d94e1..6ab95e2692a 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -290,6 +290,7 @@ typedef struct { event_type_t type; size_t completed; size_t total; + bool is_terminal; char message[LOG_MSG_SIZE]; atomic_bool ready; } event_t; @@ -339,6 +340,7 @@ static void telemetry_init_time(telemetry_t *, size_t, long); static void telemetry_init_percent(telemetry_t *, size_t, size_t); static void enqueue_event(telemetry_t *, event_t *); static void telemetry_enqueue_final_progress(telemetry_t *); +static void telemetry_enqueue_final_counter(telemetry_t *); static void telemetry_log(telemetry_t *, const char *); static void telemetry_set_info_format(telemetry_t *); static void telemetry_install_default_sink(telemetry_t *t); @@ -409,6 +411,27 @@ GProgressContext *G_progress_context_create_time(size_t total_num_elements, return context_create(total_num_elements, 0, interval_ms); } +/*! + \brief Creates an isolated progress-reporting context for open-ended + increments. + + This convenience constructor creates a context intended for + `G_progress_increment()` when the total number of work units is not known up + front. The returned context behaves as a simple counter and emits updates + whenever the completed count advances by at least `increment_step` units. + + \param increment_step Minimum completed-count increment that triggers a + progress event. + + \return A newly allocated `GProgressContext`, or `NULL` if output is + silenced by the current runtime configuration. +*/ +GProgressContext *G_progress_context_create_increment(size_t increment_step) +{ + assert(increment_step > 0); + return context_create(0, increment_step, 0); +} + /*! \brief Destroys a `GPercentContext` and releases any resources it owns. @@ -431,10 +454,17 @@ void G_progress_context_destroy(GProgressContext *ctx) return; } - if (atomic_load_explicit(&ctx->telemetry.completed, memory_order_acquire) >= - ctx->telemetry.total && - atomic_load_explicit(&ctx->telemetry.next_percent_threshold, - memory_order_acquire) <= 100) { + if (ctx->telemetry.total == 0) { + if (atomic_load_explicit(&ctx->telemetry.completed, + memory_order_acquire) > 0) { + telemetry_enqueue_final_counter(&ctx->telemetry); + } + } + else if (atomic_load_explicit(&ctx->telemetry.completed, + memory_order_acquire) >= + ctx->telemetry.total && + atomic_load_explicit(&ctx->telemetry.next_percent_threshold, + memory_order_acquire) <= 100) { telemetry_enqueue_final_progress(&ctx->telemetry); } @@ -544,19 +574,14 @@ void G_progress_update(GProgressContext *ctx, size_t completed) ctx->report_progress(t, completed); } -void G_progress_increment(GProgressContext *ctx, size_t step) +void G_progress_increment(GProgressContext *ctx, size_t completed) { - if (!ctx || step == 0) + if (!ctx) return; if (!atomic_load_explicit(&ctx->initialized, memory_order_acquire)) return; - telemetry_progress(&ctx->telemetry, step); -} - -void G_progress_tick(GProgressContext *ctx) -{ - G_progress_increment(ctx, 1); + telemetry_progress(&ctx->telemetry, completed); } void G_progress_log(GProgressContext *ctx, const char *message) @@ -595,7 +620,7 @@ static GProgressContext *context_create(size_t total_num_elements, size_t step, atomic_init(&ctx->initialized, true); - assert(step <= 100); + assert(step == 0 || total_num_elements == 0 || step <= 100); if (step == 0) { assert(interval_ms > 0); @@ -724,7 +749,8 @@ static void *telemetry_consumer(void *arg) double pct = (ev->total > 0) ? (double)ev->completed * 100.0 / (double)ev->total : 0.0; - bool is_terminal = (ev->total > 0 && ev->completed >= ev->total); + bool is_terminal = ev->is_terminal || + (ev->total > 0 && ev->completed >= ev->total); if (t->sink.on_progress) { GProgressEvent pe = { @@ -888,6 +914,18 @@ static void telemetry_enqueue_final_progress(telemetry_t *t) ev.type = EV_PROGRESS; ev.completed = t->total; ev.total = t->total; + ev.is_terminal = true; + enqueue_event(t, &ev); +} + +static void telemetry_enqueue_final_counter(telemetry_t *t) +{ + event_t ev = {0}; + + ev.type = EV_PROGRESS; + ev.completed = atomic_load_explicit(&t->completed, memory_order_acquire); + ev.total = 0; + ev.is_terminal = true; enqueue_event(t, &ev); } @@ -929,29 +967,32 @@ static void telemetry_set_info_format(telemetry_t *t) \brief Records completed work and enqueues a progress event when the next reportable threshold is reached. - The function atomically increments the telemetry's completed counter by - `step`, then decides whether to emit a progress event using one of two - modes: percent-based reporting when `percent_step` and `total` are - configured, or time-based throttling when they are not. Atomic - compare-and-swap operations ensure that only one caller emits an event for a - given threshold or interval. + The function stores the current completed count in the telemetry state, then + decides whether to emit a progress event using one of two modes: + percent-based reporting when `percent_step` and `total` are configured, or + count-based/time-based throttling when they are not. Atomic compare-and-swap + operations ensure that only one caller emits an event for a given threshold + or interval. \param t The telemetry state to update and publish through. - \param step The number of newly completed units of work to add. + \param completed The current completed count. */ -static void telemetry_progress(telemetry_t *t, size_t step) +static void telemetry_progress(telemetry_t *t, size_t completed) { - size_t new_completed = - atomic_fetch_add_explicit(&t->completed, step, memory_order_relaxed) + - step; + size_t previous = atomic_load_explicit(&t->completed, memory_order_acquire); + if (completed <= previous) { + return; + } + + atomic_store_explicit(&t->completed, completed, memory_order_release); if (t->percent_step > 0 && t->total > 0) { - if (new_completed >= t->total) { + if (completed >= t->total) { telemetry_enqueue_final_progress(t); return; } - size_t current_pct = (size_t)((new_completed * 100) / t->total); + size_t current_pct = (size_t)((completed * 100) / t->total); size_t expected = atomic_load_explicit(&t->next_percent_threshold, memory_order_relaxed); while (current_pct >= expected && expected <= 100) { @@ -974,6 +1015,21 @@ static void telemetry_progress(telemetry_t *t, size_t step) return; } } + else if (t->percent_step > 0) { + size_t expected = atomic_load_explicit(&t->next_percent_threshold, + memory_order_relaxed); + while (completed >= expected) { + size_t next = expected + t->percent_step; + if (atomic_compare_exchange_strong_explicit( + &t->next_percent_threshold, &expected, next, + memory_order_acq_rel, memory_order_relaxed)) { + break; + } + } + if (completed < expected) { + return; + } + } else { long now = now_ns(); long last = @@ -990,7 +1046,7 @@ static void telemetry_progress(telemetry_t *t, size_t step) event_t ev = {0}; ev.type = EV_PROGRESS; - ev.completed = new_completed; + ev.completed = completed; ev.total = t->total; enqueue_event(t, &ev); @@ -1156,7 +1212,10 @@ static void sink_progress_standard(const GProgressEvent *e, void *ud) { (void)ud; if (e->total == 0) { - fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", e->completed); + if (!e->is_terminal) + fprintf(stderr, "%10zu\b\b\b\b\b\b\b\b\b\b", e->completed); + else + fprintf(stderr, "\n"); return; } int pct = (int)(e->percent); @@ -1169,7 +1228,10 @@ static void sink_progress_plain(const GProgressEvent *e, void *ud) { (void)ud; if (e->total == 0) { - fprintf(stderr, "%zu..", e->completed); + if (e->is_terminal) + fprintf(stderr, "%s", "\n"); + else + fprintf(stderr, "%zu..", e->completed); return; } int pct = (int)(e->percent); @@ -1180,8 +1242,10 @@ static void sink_progress_gui(const GProgressEvent *e, void *ud) { (void)ud; if (e->total == 0) { - fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", e->completed); - fflush(stderr); + if (!e->is_terminal) { + fprintf(stderr, "GRASS_INFO_PROGRESS: %zu\n", e->completed); + fflush(stderr); + } return; } int pct = (int)(e->percent); From 2c8af78cd96fd8290695f721d2dabb4d4d6378d4 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Thu, 2 Apr 2026 08:43:48 +0200 Subject: [PATCH 23/23] fix indefinite progress 2 --- lib/gis/percent.c | 80 +++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/lib/gis/percent.c b/lib/gis/percent.c index 6ab95e2692a..8d5115a701d 100644 --- a/lib/gis/percent.c +++ b/lib/gis/percent.c @@ -213,8 +213,6 @@ void G_percent_reset(void) \param n current element \param s increment size - - \return always returns 0 */ void G_progress(long n, int s) { @@ -574,6 +572,27 @@ void G_progress_update(GProgressContext *ctx, size_t completed) ctx->report_progress(t, completed); } +/** + \brief Reports progress for open-ended loops. + + Use this function with contexts created by + `G_progress_context_create_increment()` when work is tracked as an open-ended + completed count instead of progress toward a known total. The function + ignores `NULL` or uninitialized contexts and forwards the completed-count + update to the context telemetry. + + Example: + ```c + GProgressContext *ctx = G_progress_context_create_increment(100); + while (TRUE) { + G_progress_increment(ctx, rows_processed); + } + ``` + + \param ctx The increment-based progress context to update. + \param completed The current completed count used to determine whether a new + progress event should be emitted. +*/ void G_progress_increment(GProgressContext *ctx, size_t completed) { if (!ctx) @@ -584,6 +603,24 @@ void G_progress_increment(GProgressContext *ctx, size_t completed) telemetry_progress(&ctx->telemetry, completed); } +/** + \brief Enqueues a log message for a progress reporting context. + + Use this function to attach informational output to an active + `GProgressContext`. The message is forwarded to the context telemetry and + emitted by the configured progress sink in the order it is queued. The + function ignores `NULL` messages and contexts that are `NULL` or not yet + initialized. + + Example: + ```c + GProgressContext *ctx = G_progress_context_create_time(total_rows, 100); + G_progress_log(ctx, _("Starting import")); + ``` + + \param ctx The progress context that should receive the log message. + \param message A null-terminated message string to enqueue for output. +*/ void G_progress_log(GProgressContext *ctx, const char *message) { if (!ctx || !message) @@ -1349,9 +1386,8 @@ static void G__percent_reset_ng(void) // Print progress info messages static void G__progress_ng(long n, int s) { - // Mirror legacy behavior: emit on multiples of s, and handle first tick - // formatting. We route through the global telemetry so it benefits from the - // consumer thread. + // Mirror legacy behavior: emit on multiples of s. Routing through the + // global telemetry. if (s <= 0 || output_is_silenced()) return; @@ -1360,27 +1396,15 @@ static void G__progress_ng(long n, int s) // percent thresholds start_global_percent(0, 0); - // Use time-based gating if an interval is configured; otherwise, we emit - // only on multiples of s. Here, we implement the modulo gating explicitly. - if (n == s && n == 1) { - // For default sink, legacy prints a leading CR/Newline depending on - // format. We simulate this by enqueueing a LOG event when using default - // sink; custom sinks can ignore. - if (g_percent_telemetry.sink.on_progress == NULL) { - switch (g_percent_telemetry.info_format) { - case G_INFO_FORMAT_PLAIN: - telemetry_log(&g_percent_telemetry, "\n"); - break; - case G_INFO_FORMAT_GUI: - // No-op; GUI variant prints on progress events - break; - default: - // STANDARD and others: carriage return - telemetry_log(&g_percent_telemetry, "\r"); - break; - } - } + event_t ev = {0}; + + ev.type = EV_PROGRESS; + ev.completed = atomic_load_explicit(&g_percent_telemetry.completed, + memory_order_acquire); + ev.total = 0; + ev.is_terminal = true; + enqueue_event(&g_percent_telemetry, &ev); return; } @@ -1392,6 +1416,8 @@ static void G__progress_ng(long n, int s) ev.type = EV_PROGRESS; ev.completed = (n < 0 ? 0 : (size_t)n); ev.total = 0; // unknown total; consumer/sink can render raw counts + atomic_store_explicit(&g_percent_telemetry.completed, ev.completed, + memory_order_release); enqueue_event(&g_percent_telemetry, &ev); } @@ -1399,8 +1425,8 @@ static void G__progress_ng(long n, int s) static void G__set_percent_routine_ng(int (*fn)(int)) { // The historical signature in gis.h declares int (*)(int), but actual - // implementers often used void(*)(int). We accept int-returning and ignore - // the return value. + // implementers often used void(*)(int). We accept int-returning and + // ignore the return value. if (!fn) { // Reset to default behavior g_legacy_percent_routine = NULL;