HIPO  4.3.0
High Performance Output data format for experimental physics
progresstracker.hpp
Go to the documentation of this file.
1 
9 #pragma once
10 
11 #include <fmt/core.h>
12 #include <fmt/format.h>
13 #include <algorithm>
14 #include <atomic>
15 #include <chrono>
16 #include <cstddef>
17 #include <iostream>
18 #include <mutex>
19 #include <stdexcept>
20 #include <string>
21 #include <string_view>
22 #include <thread>
23 
24 namespace detail {
27 inline std::recursive_mutex& get_stdout_mutex() {
28  static std::recursive_mutex s_stdout_mutex;
29  return s_stdout_mutex;
30 }
31 } // namespace detail
32 
38  public:
45  struct Config {
46  std::size_t bar_width;
47  bool show_eta;
48  bool show_rate;
49  bool use_colors;
50  bool use_gradient;
51  std::string label;
52  std::chrono::milliseconds update_interval;
53 
56  : bar_width(50), show_eta(true), show_rate(true), use_colors(true), use_gradient(true), label("Processing"), update_interval(100) {}
57  };
58 
65  explicit ProgressTracker(std::size_t total, Config config = {});
66 
67  ~ProgressTracker() noexcept;
68 
69  // Non-copyable, move-only
70  ProgressTracker(const ProgressTracker&) = delete;
71  ProgressTracker& operator=(const ProgressTracker&) = delete;
72  ProgressTracker(ProgressTracker&&) noexcept;
73  ProgressTracker& operator=(ProgressTracker&&) noexcept;
74 
78  auto increment() noexcept -> void;
79 
83  auto add(std::size_t count) noexcept -> void;
84 
88  [[nodiscard]] auto get_processed() const noexcept -> std::size_t {
89  return m_processed.load(std::memory_order_acquire);
90  }
91 
95  [[nodiscard]] auto get_total() const noexcept -> std::size_t {
96  return m_total;
97  }
98 
102  [[nodiscard]] auto get_progress() const noexcept -> double;
103 
107  auto start() noexcept -> void;
108 
112  auto finish() noexcept -> void;
113 
117  static auto clear_line() noexcept -> void;
118 
130  [[nodiscard]] static auto format_duration(std::chrono::seconds duration) noexcept -> std::string;
131 
132  private:
133  // ANSI escape codes
134  struct AnsiCodes {
135  static constexpr std::string_view CLEAR_LINE = "\r\033[K";
136  static constexpr std::string_view RESET = "\033[0m";
137  static constexpr std::string_view BOLD = "\033[1m";
138  static constexpr std::string_view DIM = "\033[2m";
139 
140  // Standard colors
141  static constexpr std::string_view GREEN = "\033[32m";
142  static constexpr std::string_view CYAN = "\033[36m";
143  static constexpr std::string_view YELLOW = "\033[33m";
144  static constexpr std::string_view MAGENTA = "\033[35m";
145  static constexpr std::string_view WHITE = "\033[37m";
146  static constexpr std::string_view BLUE = "\033[34m";
147  static constexpr std::string_view RED = "\033[31m";
148 
149  // Bright colors
150  static constexpr std::string_view BRIGHT_GREEN = "\033[92m";
151  static constexpr std::string_view BRIGHT_CYAN = "\033[96m";
152  static constexpr std::string_view BRIGHT_YELLOW = "\033[93m";
153  static constexpr std::string_view BRIGHT_MAGENTA = "\033[95m";
154  static constexpr std::string_view BRIGHT_WHITE = "\033[97m";
155 
156  // Background colors for gradient effect
157  static constexpr std::string_view BG_CYAN = "\033[46m";
158  static constexpr std::string_view BG_BLUE = "\033[44m";
159  static constexpr std::string_view BG_MAGENTA = "\033[45m";
160  };
161 
162  // Progress bar block characters (8 levels of granularity)
163  static constexpr std::array<std::string_view, 9> BLOCK_CHARS = {
164  " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"};
165 
166  // Spinner animation characters
167  static constexpr std::array<std::string_view, 10> SPINNER_CHARS = {
168  "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
169 
171  auto update_display() noexcept -> void;
174  auto render_bar(double progress) const noexcept -> void;
176  auto update_loop() noexcept -> void;
178  auto stop_update_thread() noexcept -> void;
179 
183  [[nodiscard]] static auto format_number(std::size_t num) noexcept -> std::string;
186  [[nodiscard]] auto get_elapsed() const noexcept -> std::chrono::seconds;
189  [[nodiscard]] auto calculate_eta() const noexcept -> std::chrono::seconds;
192  [[nodiscard]] auto calculate_rate() const noexcept -> double;
195  [[nodiscard]] auto get_progress_color() const noexcept -> std::string_view;
196 
197  const std::size_t m_total;
198  std::atomic<std::size_t> m_processed{0};
199  std::chrono::steady_clock::time_point m_start_time;
200  Config m_config;
201  mutable std::mutex m_display_mutex;
202  bool m_finished{false};
203  mutable std::size_t m_spinner_index{0};
204 
205  // Background update thread
206  std::atomic<bool> m_should_stop{false};
207  std::thread m_update_thread;
208 };
209 
210 inline ProgressTracker::ProgressTracker(std::size_t total, Config config)
211  : m_total{total}, m_start_time{std::chrono::steady_clock::now()}, m_config{config} {
212  if (total == 0) throw std::invalid_argument("ProgressTracker: total must be greater than 0");
213 }
214 
216  stop_update_thread();
217  if (!m_finished) clear_line();
218 }
219 
221  : m_total{other.m_total}, m_processed{other.m_processed.load(std::memory_order_acquire)}, m_start_time{other.m_start_time}, m_config{other.m_config}, m_finished{other.m_finished} {
222  other.stop_update_thread();
223  other.m_finished = true;
224 }
225 
227  if (this != &other) {
228  stop_update_thread();
229  if (!m_finished) clear_line();
230  other.stop_update_thread();
231 
232  const_cast<std::size_t&>(m_total) = other.m_total;
233  m_processed.store(other.m_processed.load(std::memory_order_acquire), std::memory_order_release);
234  m_start_time = other.m_start_time;
235  m_config = other.m_config;
236  m_finished = other.m_finished;
237 
238  other.m_finished = true;
239  }
240  return *this;
241 }
242 
243 inline auto ProgressTracker::increment() noexcept -> void {
244  m_processed.fetch_add(1, std::memory_order_acq_rel);
245 }
246 
247 inline auto ProgressTracker::add(std::size_t count) noexcept -> void {
248  m_processed.fetch_add(count, std::memory_order_acq_rel);
249 }
250 
251 inline auto ProgressTracker::get_progress() const noexcept -> double {
252  const auto current = m_processed.load(std::memory_order_acquire);
253  return m_total > 0 ? static_cast<double>(current) / static_cast<double>(m_total) : 0.0;
254 }
255 
256 inline auto ProgressTracker::get_elapsed() const noexcept -> std::chrono::seconds {
257  const auto elapsed = std::chrono::steady_clock::now() - m_start_time;
258  return std::chrono::duration_cast<std::chrono::seconds>(elapsed);
259 }
260 
261 inline auto ProgressTracker::calculate_rate() const noexcept -> double {
262  const auto elapsed = get_elapsed().count();
263  const auto current = m_processed.load(std::memory_order_acquire);
264  return elapsed > 0 ? static_cast<double>(current) / static_cast<double>(elapsed) : 0.0;
265 }
266 
267 inline auto ProgressTracker::calculate_eta() const noexcept -> std::chrono::seconds {
268  const auto current = m_processed.load(std::memory_order_acquire);
269  const auto rate = calculate_rate();
270 
271  if (rate > 0.0 && current < m_total) {
272  const auto remaining = m_total - current;
273  return std::chrono::seconds{static_cast<long>(remaining / rate)};
274  }
275  return std::chrono::seconds{0};
276 }
277 
278 inline auto ProgressTracker::format_duration(std::chrono::seconds duration) noexcept -> std::string {
279  const auto m_totalseconds = duration.count();
280 
281  if (m_totalseconds < 60) return fmt::format("{}s", m_totalseconds);
282  if (m_totalseconds < 3600) {
283  const auto minutes = m_totalseconds / 60;
284  const auto seconds = m_totalseconds % 60;
285  return fmt::format("{}m {:02d}s", minutes, seconds);
286  }
287 
288  const auto hours = m_totalseconds / 3600;
289  const auto minutes = (m_totalseconds % 3600) / 60;
290  return fmt::format("{}h {:02d}m", hours, minutes);
291 }
292 
293 inline auto ProgressTracker::format_number(std::size_t num) noexcept -> std::string {
294  if (num < 1000) return fmt::format("{}", num);
295  if (num < 1000000) return fmt::format("{:.1f}k", num / 1000.0);
296  return fmt::format("{:.1f}M", num / 1000000.0);
297 }
298 
299 inline auto ProgressTracker::clear_line() noexcept -> void {
300  std::lock_guard lock{detail::get_stdout_mutex()};
301  try {
302  fmt::print("{}", AnsiCodes::CLEAR_LINE);
303  std::cout.flush();
304  } catch (...) {
305  }
306 }
307 
308 inline auto ProgressTracker::get_progress_color() const noexcept -> std::string_view {
309  if (!m_config.use_colors || !m_config.use_gradient) return AnsiCodes::GREEN;
310  return AnsiCodes::BRIGHT_GREEN;
311 }
312 
313 inline auto ProgressTracker::render_bar(double progress) const noexcept -> void {
314  std::lock_guard lock{detail::get_stdout_mutex()};
315  try {
316  const auto& colors = m_config.use_colors;
317  progress = std::clamp(progress, 0.0, 1.0);
318 
319  const auto filled_width = m_config.bar_width * progress;
320  const auto full_blocks = static_cast<std::size_t>(filled_width);
321  const auto remainder = filled_width - static_cast<double>(full_blocks);
322  const auto partial_block_idx = static_cast<std::size_t>(remainder * 8.0);
323 
324  // Left bracket with color
325  if (colors) fmt::print("{}", AnsiCodes::DIM);
326  fmt::print("╢");
327  if (colors) fmt::print("{}", AnsiCodes::RESET);
328 
329  // Render progress bar with gradient
330  for (std::size_t i = 0; i < m_config.bar_width; ++i) {
331  if (i < full_blocks) {
332  if (colors) {
333  fmt::print("{}", get_progress_color());
334  }
335  fmt::print("{}", BLOCK_CHARS[8]);
336  } else if (i == full_blocks) {
337  if (colors) {
338  fmt::print("{}", get_progress_color());
339  }
340  fmt::print("{}", BLOCK_CHARS[partial_block_idx]);
341  } else {
342  if (colors) fmt::print("{}", AnsiCodes::DIM);
343  fmt::print("░");
344  }
345  }
346 
347  if (colors) fmt::print("{}", AnsiCodes::RESET);
348 
349  // Right bracket
350  if (colors) fmt::print("{}", AnsiCodes::DIM);
351  fmt::print("╟");
352  if (colors) fmt::print("{}", AnsiCodes::RESET);
353  fmt::print(" ");
354 
355  } catch (...) {
356  }
357 }
358 
359 inline auto ProgressTracker::update_display() noexcept -> void {
360  std::lock_guard lock{m_display_mutex};
361 
362  if (m_finished) return;
363 
364  try {
365  const auto current = m_processed.load(std::memory_order_acquire);
366  const auto progress = get_progress();
367  const auto elapsed = get_elapsed();
368  const auto& colors = m_config.use_colors;
369 
370  // Lock stdout mutex for all print operations
371  std::lock_guard stdout_lock{detail::get_stdout_mutex()};
372 
373  clear_line();
374 
375  // Spinner animation
376  if (progress < 1.0) {
377  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_CYAN);
378  fmt::print("{}", SPINNER_CHARS[m_spinner_index % SPINNER_CHARS.size()]);
379  m_spinner_index++;
380  if (colors) fmt::print("{}", AnsiCodes::RESET);
381  fmt::print(" ");
382  } else {
383  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_GREEN);
384  fmt::print("✓");
385  if (colors) fmt::print("{}", AnsiCodes::RESET);
386  fmt::print(" ");
387  }
388 
389  // Label
390  if (!m_config.label.empty()) {
391  if (colors) fmt::print("{}", AnsiCodes::BOLD);
392  fmt::print("{}", m_config.label);
393  if (colors) fmt::print("{}", AnsiCodes::RESET);
394  fmt::print(" ");
395  }
396 
397  render_bar(progress);
398 
399  // Percentage with enhanced styling
400  if (progress >= 1.0) {
401  if (colors) fmt::print("{}{}", AnsiCodes::BRIGHT_GREEN, AnsiCodes::BOLD);
402  fmt::print("100%");
403  if (colors) fmt::print("{}", AnsiCodes::RESET);
404  } else {
405  if (colors) fmt::print("{}", get_progress_color());
406  fmt::print("{:>3.0f}%", progress * 100.0);
407  if (colors) fmt::print("{}", AnsiCodes::RESET);
408  }
409 
410  // Count with dividers
411  fmt::print(" ");
412  if (colors) fmt::print("{}", AnsiCodes::DIM);
413  fmt::print("│");
414  if (colors) fmt::print("{}", AnsiCodes::RESET);
415  fmt::print(" ");
416 
417  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_WHITE);
418  fmt::print("{}", format_number(current));
419  if (colors) fmt::print("{}", AnsiCodes::RESET);
420 
421  if (colors) fmt::print("{}", AnsiCodes::DIM);
422  fmt::print("/");
423  if (colors) fmt::print("{}", AnsiCodes::RESET);
424 
425  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_CYAN);
426  fmt::print("{}", format_number(m_total));
427  if (colors) fmt::print("{}", AnsiCodes::RESET);
428 
429  // Elapsed time
430  fmt::print(" ");
431  if (colors) fmt::print("{}", AnsiCodes::DIM);
432  fmt::print("│ ⏱");
433  if (colors) fmt::print("{}", AnsiCodes::RESET);
434  fmt::print(" ");
435  if (colors) fmt::print("{}", AnsiCodes::MAGENTA);
436  fmt::print("{}", format_duration(elapsed));
437  if (colors) fmt::print("{}", AnsiCodes::RESET);
438 
439  // ETA
440  if (m_config.show_eta && current < m_total) {
441  const auto eta = calculate_eta();
442  if (eta.count() > 0) {
443  fmt::print(" ");
444  if (colors) fmt::print("{}", AnsiCodes::DIM);
445  fmt::print("│ ⏳");
446  if (colors) fmt::print("{}", AnsiCodes::RESET);
447  fmt::print(" ");
448  if (colors) fmt::print("{}", AnsiCodes::YELLOW);
449  fmt::print("{}", format_duration(eta));
450  if (colors) fmt::print("{}", AnsiCodes::RESET);
451  }
452  }
453 
454  // Rate
455  if (m_config.show_rate) {
456  const auto rate = calculate_rate();
457  if (rate > 0.0) {
458  fmt::print(" ");
459  if (colors) fmt::print("{}", AnsiCodes::DIM);
460  fmt::print("│ ⚡");
461  if (colors) fmt::print("{}", AnsiCodes::RESET);
462  fmt::print(" ");
463  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_MAGENTA);
464  fmt::print("{:.1f}/s", rate);
465  if (colors) fmt::print("{}", AnsiCodes::RESET);
466  }
467  }
468 
469  std::cout.flush();
470 
471  } catch (...) {
472  }
473 }
474 
475 inline auto ProgressTracker::update_loop() noexcept -> void {
476  while (!m_should_stop.load(std::memory_order_acquire)) {
477  update_display();
478  if (m_processed.load(std::memory_order_acquire) >= m_total) break;
479  std::this_thread::sleep_for(m_config.update_interval);
480  }
481 
482  update_display();
483 }
484 
485 inline auto ProgressTracker::start() noexcept -> void {
486  if (!m_update_thread.joinable() && !m_finished) {
487  m_should_stop.store(false, std::memory_order_release);
488  update_display();
489  m_update_thread = std::thread(&ProgressTracker::update_loop, this);
490  }
491 }
492 
493 inline auto ProgressTracker::stop_update_thread() noexcept -> void {
494  if (m_update_thread.joinable()) {
495  m_should_stop.store(true, std::memory_order_release);
496  m_update_thread.join();
497  }
498 }
499 
500 inline auto ProgressTracker::finish() noexcept -> void {
501  stop_update_thread();
502 
503  std::lock_guard lock{m_display_mutex};
504 
505  if (m_finished) return;
506  m_finished = true;
507 
508  try {
509  const auto elapsed = get_elapsed();
510  const auto& colors = m_config.use_colors;
511 
512  // Lock stdout mutex for all print operations
513  std::lock_guard stdout_lock{detail::get_stdout_mutex()};
514 
515  clear_line();
516 
517  // Completion line
518  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_GREEN);
519  fmt::print(" {} Complete! │ ", m_config.label);
520  if (colors) fmt::print("{}", AnsiCodes::RESET);
521 
522  // Files processed
523  fmt::print(" Items: ");
524  if (colors) fmt::print("{}", AnsiCodes::BRIGHT_CYAN);
525  fmt::print("{}", format_number(m_total));
526  if (colors) fmt::print("{}", AnsiCodes::RESET);
527  fmt::print(" │ ");
528 
529  // Time elapsed
530  fmt::print("⏱ Time: ");
531  if (colors) fmt::print("{}", AnsiCodes::MAGENTA);
532  fmt::print("{}", format_duration(elapsed));
533  if (colors) fmt::print("{}", AnsiCodes::RESET);
534 
535  fmt::print("\n");
536  std::cout.flush();
537 
538  } catch (...) {
539  }
540 }
Thread-safe progress tracker with visual progress bar.
auto get_total() const noexcept -> std::size_t
Gets total item count (thread-safe).
static auto clear_line() noexcept -> void
Clears the current progress line.
auto increment() noexcept -> void
Increments the progress counter (thread-safe, no immediate display update).
auto get_processed() const noexcept -> std::size_t
Gets current progress count (thread-safe).
ProgressTracker & operator=(const ProgressTracker &)=delete
~ProgressTracker() noexcept
auto add(std::size_t count) noexcept -> void
Adds multiple items to progress (thread-safe, no immediate display update).
static auto format_duration(std::chrono::seconds duration) noexcept -> std::string
Formats a duration given in seconds into a human-readable string.
auto start() noexcept -> void
Starts the background update thread and shows initial display.
auto get_progress() const noexcept -> double
Gets current progress ratio [0.0, 1.0] (thread-safe).
auto finish() noexcept -> void
Stops the update thread and displays final completion message.
ProgressTracker(std::size_t total, Config config={})
Constructs a progress tracker.
std::recursive_mutex & get_stdout_mutex()
Returns the global recursive mutex used to synchronize stdout access. A recursive mutex is used to al...
Configuration settings for progress bar display.
bool use_gradient
Use gradient coloring for progress bar (default: true)
Config()
Default constructor initializes all fields to reasonable defaults.
std::size_t bar_width
Width of the progress bar in characters (default: 50)
std::string label
Label text displayed before the progress bar (default: "Processing")
bool show_rate
Display processing rate in items per second (default: true)
std::chrono::milliseconds update_interval
Time between display updates (default: 100ms)
bool show_eta
Display estimated time to completion (default: true)
bool use_colors
Use ANSI color codes in output (default: true)