clix
Single-header file CLI libraries for C
Loading...
Searching...
No Matches
progress.h
1// SPDX-License-Identifier: MIT
2/**
3 * @page progress.h
4 * @author Jakov Dragičević (github:jakovdev)
5 * @copyright MIT License.
6 * @attention This library is work-in-progress. Do not use if not meaning to contribute.
7 * @brief Progress bar display for parallel workloads.
8 *
9 * Before the parallel region (main thread):
10 * 1. Call @ref progress_start.
11 *
12 * Inside the parallel region (all worker threads):
13 * 2. Call @ref progress_add inside the outer-most loop.
14 * 3. Call @ref progress_flush outside the outer-most loop.
15 *
16 * After the parallel region (main thread):
17 * 4. Call @ref progress_end.
18 *
19 * Example #1:
20 * @code{.c}
21 * progress_start(total_items, "Processing items");
22 * #pragma omp parallel
23 * {
24 * #pragma omp for
25 * for (int i = 0; i < total_items; i++) {
26 * process_item(i);
27 * progress_add(1);
28 * }
29 * progress_flush();
30 * }
31 * progress_end();
32 * @endcode
33 *
34 * Example #2:
35 * @code{.c}
36 * progress_start(total_items, "Processing matrix");
37 * double time_begin = current_time();
38 * #pragma omp parallel
39 * {
40 * #pragma omp for
41 * for (int i = 0; i < rows; i++) {
42 * for (int j = 0; j < cols; j++) {
43 * process_matrix_cell(i, j);
44 * }
45 * // items_per_row depends on the inner loop
46 * const s64 items_per_row = cols;
47 * progress_add(items_per_row);
48 * }
49 * progress_flush();
50 * }
51 * double time_end = current_time();
52 * progress_end();
53 * printf("Matrix processing time: %.2f", time_end - time_begin);
54 * @endcode
55 *
56 * Limitations:
57 * - Only one progress monitor thread can be active at a time.
58 * - @ref progress_end may cause slight delays from mutex contention.
59 * - Requires C11 (atomics) to build the source file. On Windows https://github.com/tinycthread/tinycthread is recommended.
60 *
61 * Source code:
62 * @include progress.h
63 */
64
65#ifndef PROGRESS_H_
66#define PROGRESS_H_
67
68#include <stdbool.h>
69#include <stddef.h>
70
71/**
72 * @brief Starts the progress monitor thread.
73 *
74 * Creates a background thread that updates the progress bar periodically.
75 * Must be called from the main thread before starting parallel work.
76 * Automatically adjusts update frequency based on @p total.
77 *
78 * @param total Total number of work units to complete.
79 * @param threads Number of threads that will be performing work.
80 * @param message Description displayed alongside the progress bar.
81 * @return false if thread creation fails or already running, true otherwise.
82 */
83bool progress_start(size_t total, int threads, const char *message);
84
85/**
86 * @brief Increments the progress counter.
87 *
88 * Must be called from all threads that perform work out of @p total.
89 * Progress is batched locally to minimize atomic operations.
90 * Does not guarantee immediate update if @p amount too small.
91 * Recommended to call in the outer-most loop with highest @p amount possible.
92 *
93 * @param amount Number of thread-local work units completed out of @p total.
94 */
95void progress_add(size_t amount);
96
97/**
98 * @brief Flushes thread-local progress.
99 *
100 * Call once per thread after it finishes its work so that progress bar
101 * reaches 100% as some @p amount may still be buffered.
102 */
103void progress_flush(void);
104
105/**
106 * @brief Stops the progress monitor thread.
107 *
108 * Must be called from the main thread after all parallel work completes.
109 * May be slightly delayed due to mutex contention.
110 */
111bool progress_end(void);
112
113#ifdef PROGRESS_EXTERN_DISABLE
114extern bool progress_disable;
115#endif
116
117#endif /* PROGRESS_H_ */
118#if defined(PROGRESS_IMPLEMENTATION) && !defined(_PROGRESS_IMPLEMENTED)
119#define _PROGRESS_IMPLEMENTED
120
121#include <stdalign.h>
122#include <stdatomic.h>
123#include <threads.h>
124
125#ifndef PROGRESS_PRINT_H
126#define PROGRESS_PRINT_H 0
127#include <stdio.h>
128#else
129#ifndef PRINT_H_
130#error "Include print.h before including progress.h"
131#endif /* PRINT_H_ */
132#undef PROGRESS_PRINT_H
133#define PROGRESS_PRINT_H 1
134#endif /* PROGRESS_PRINT_H */
135
136#ifndef progress_bar
137#if !PROGRESS_PRINT_H
138#define progress_bar(pct, ...) \
139 do { \
140 printf("\r%3d%% ", pct); \
141 printf(__VA_ARGS__); \
142 fflush(stdout); \
143 } while (0)
144#endif /* progress_bar is a function */
145#endif /* progress_bar */
146
147#ifndef progress_err
148#if PROGRESS_PRINT_H
149#define progress_err(...) perr(__VA_ARGS__)
150#else /* Default */
151#define progress_err(...) fprintf(stderr, __VA_ARGS__)
152#endif /* PROGRESS_PRINT_H */
153#endif /* progress_err */
154
155#ifndef progress_dev
156#if PROGRESS_PRINT_H
157#define progress_dev(...) pdev(__VA_ARGS__)
158#else /* Default */
159#define progress_dev(...) fprintf(stderr, __VA_ARGS__)
160#endif /* PROGRESS_PRINT_H */
161#endif /* progress_dev */
162
163static _Thread_local size_t t_done;
164
165static _Alignas(64) _Atomic(size_t) p_done;
166static thrd_t p_monitor_thrd;
167
168static size_t p_total;
169static size_t p_update_limit;
170static const char *p_message;
171
172static _Atomic(bool) p_running;
173static bool progress_disable;
174
175static mtx_t p_mutex;
176static cnd_t p_cond;
177
178static int p_monitor(void *arg)
179{
180 (void)arg;
181 progress_bar(0, "%s", p_message);
182
183 struct timespec timeout;
184
185 size_t current = 0;
186 while (atomic_load_explicit(&p_running, memory_order_acquire)) {
187 current = atomic_load_explicit(&p_done, memory_order_relaxed);
188 progress_bar((int)(100 * current / p_total), "%s", p_message);
189
190 if (timespec_get(&timeout, TIME_UTC) == 0) {
191 timeout.tv_sec = 0;
192 timeout.tv_nsec = 0;
193 }
194 timeout.tv_nsec += 250000000;
195 if (timeout.tv_nsec >= 1000000000) {
196 timeout.tv_sec++;
197 timeout.tv_nsec -= 1000000000;
198 }
199
200 mtx_lock(&p_mutex);
201 cnd_timedwait(&p_cond, &p_mutex, &timeout);
202 mtx_unlock(&p_mutex);
203 }
204
205 progress_bar(100, "%s", p_message);
206 return thrd_success;
207}
208
209bool progress_start(size_t total, int threads, const char *message)
210{
211 if (progress_disable)
212 return true;
213
214 if (atomic_load_explicit(&p_running, memory_order_relaxed))
215 goto p_monitor_running_error;
216
217 atomic_store_explicit(&p_running, true, memory_order_relaxed);
218 atomic_store_explicit(&p_done, 0, memory_order_relaxed);
219 p_message = message;
220 p_total = total;
221 if (!p_total)
222 p_total = 1;
223 p_update_limit = total / ((size_t)threads * 100);
224 if (!p_update_limit)
225 p_update_limit = 1;
226
227 if (mtx_init(&p_mutex, mtx_plain) != thrd_success)
228 goto p_monitor_mtx_error;
229 if (cnd_init(&p_cond) != thrd_success)
230 goto p_monitor_cnd_error;
231
232 if (thrd_create(&p_monitor_thrd, p_monitor, NULL) == thrd_success)
233 return true;
234
235 cnd_destroy(&p_cond);
236p_monitor_cnd_error:
237 mtx_destroy(&p_mutex);
238p_monitor_mtx_error:
239 atomic_store_explicit(&p_running, false, memory_order_relaxed);
240p_monitor_running_error:
241 progress_err("Failed to create a progress bar display");
242 return false;
243}
244
245void progress_flush(void)
246{
247 if (progress_disable || !t_done)
248 return;
249
250 atomic_fetch_add_explicit(&p_done, t_done, memory_order_relaxed);
251 t_done = 0;
252}
253
254void progress_add(size_t amount)
255{
256 if (progress_disable || (t_done += amount) < p_update_limit)
257 return;
258
259 progress_flush();
260}
261
262bool progress_end(void)
263{
264 if (progress_disable)
265 return true;
266
267 if (!atomic_load_explicit(&p_running, memory_order_relaxed)) {
268 progress_dev("Tried to end non-running progress monitor");
269 progress_err("Internal error during progress bar display");
270 return false;
271 }
272
273 progress_flush();
274 atomic_store_explicit(&p_done, p_total, memory_order_relaxed);
275 atomic_store_explicit(&p_running, false, memory_order_release);
276 cnd_signal(&p_cond);
277 thrd_join(p_monitor_thrd, NULL);
278 cnd_destroy(&p_cond);
279 mtx_destroy(&p_mutex);
280 return true;
281}
282
283#endif /* PROGRESS_IMPLEMENTATION */
284
285/*
286progress.h
287https://github.com/jakovdev/clix/
288Copyright (c) 2026 Jakov Dragičević
289Permission is hereby granted, free of charge, to any person obtaining a copy
290of this software and associated documentation files (the "Software"), to deal
291in the Software without restriction, including without limitation the rights
292to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
293copies of the Software, and to permit persons to whom the Software is
294furnished to do so, subject to the following conditions:
295The above copyright notice and this permission notice shall be included in all
296copies or substantial portions of the Software.
297THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
298IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
299FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
300AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
301LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
302OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
303SOFTWARE.
304*/